How to highlight matches within a string with JSX?

后端 未结 2 1989
情歌与酒
情歌与酒 2021-01-14 21:06

I have a custom autocomplete, so when you type, it will display a list of suggestions based on the input value. In the list, I would like to bold the characters that are th

相关标签:
2条回答
  • 2021-01-14 21:29

    Writing your own highlighting code could lead down a rabbit hole. In my answer, I assume only simple text (no HTML within the strings, no charset edge cases) and valid non-escaped RegExp pattern string.


    Instead of building a new string, you could build a new array, in which you could put JSX. React can render an array of nodes directly.

    The logic behind

    As a simple proof of concept, here's the logic we could use:

    const defaultHighlight = s => <em>{s}</em>;
    
    // Needed if the target includes ambiguous characters that are valid regex operators.
    const escapeRegex = v => v.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&");
    
    /**
     * Case insensitive highlight which keeps the source casing.
     * @param {string} source text
     * @param {string} target to highlight within the source text
     * @param {Function} callback to define how to highlight the text
     * @returns {Array}
     */
    const highlightWord = (source, target, callback) => {
      const res = [];
    
      if (!source) return res;
      if (!target) return source;
      
      const regex = new RegExp(escapeRegex(target), 'gi');
    
      let lastOffset = 0;
      
      // Uses replace callback, but not its return value
      source.replace(regex, (val, offset) => {
        // Push both the last part of the string, and the new part with the highlight
        res.push(
          source.substr(lastOffset, offset - lastOffset),
          // Replace the string with JSX or anything.
          (callback || defaultHighlight)(val)
        );
        lastOffset = offset + val.length;
      });
      
      // Push the last non-highlighted string
      res.push(source.substr(lastOffset));
      return res;
    };
    
    /**
     * React component that wraps our `highlightWord` util.
     */
    const Highlight = ({ source, target, children }) => 
      highlightWord(source, target, children);
    
    
    const TEXT = 'This is a test.';
    
    const Example = () => (
      <div>
        <div>Nothing: "<Highlight />"</div>
        <div>No target: "<Highlight source={TEXT} />"</div>
        <div>Default 'test': "<Highlight source={TEXT} target="test" />"</div>
        <div>Multiple custom with 't': 
          "<Highlight source={TEXT} target="t">
            {s => <span className="highlight">{s}</span>}
          </Highlight>"
        </div>
        <div>Ambiguous target '.': 
          "<Highlight source={TEXT} target=".">
            {s => <span className="highlight">{s}</span>}
          </Highlight>"
        </div>
      </div>
    );
    
    
    // Render it
    ReactDOM.render(
      <Example />,
      document.getElementById("react")
    );
    .highlight {
      background-color: yellow;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
    <div id="react"></div>

    No need to use dangerouslySetInnerHTML here.

    This highlightWord function can take any function to wrap the matched string.

    highlight(match, value) // default to `s => <em>{s}</em>`
    // or
    highlight(match, value, s => <span className="highlight">{s}</span>);
    

    I'm doing minimal regex string escaping based on another answer on Stack Overflow.


    The Highlight component

    As shown, we can create a component so it's "more react"!

    /**
     * React component that wraps our `highlightWord` util.
     */
    const Highlight = ({ source, target, children }) => 
      highlightWord(source, target, children);
    
    Highlight.propTypes = {
      source: PropTypes.string,
      target: PropTypes.string,
      children: PropTypes.func,
    };
    
    Highlight.defaultProps = {
      source: null,
      target: null,
      children: null,
    };
    
    export default Highlight;
    

    It uses a render prop, so you'd have to change your rendering to:

    <ul>
      {matches.map((match, idx) => (
        <li key={idx}>
          <Highlight source={match} target={value}>
            {s => <strong>{s}</strong>}
          </Highlight>
        </li>
      ))}
    </ul>
    
    0 讨论(0)
  • 2021-01-14 21:31

    You just append your mapper as children inside your auto complete component.

    <CustomAutocomplete>
      <ul>
        {
          matches.map(function(match, idx){
            let re = new RegExp(value, 'g');
            let str = match.replace(re, '<b>'+ value +'</b>');
            return (<li key={idx}>{str}</li>)
          })
        }
      </ul>
    </CustomAutocomplete>
    
    0 讨论(0)
提交回复
热议问题