最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to highlight matches within a string with JSX? - Stack Overflow

programmeradmin6浏览0评论

I have a custom autoplete, 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 the same as the input value.

So if I have a list of suggestions: "alligator", "lima", "lime", and I typed "li", then the suggestions would look like this:

  • alligator
  • lima
  • lime

I have this simple map in my jsx file:

<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>

where value is the input value. It displays the list but in this string format

  • al<b>li</b>gator
  • <b>li</b>ma
  • <b>li</b>me

Not sure how to go about with React. I thought of using dangerouslyinnerhtml or something like that, but I think that's a last resort thing. I would like to avoid that if possible.

This my autoplete ponent:

class Autoplete extends Component{

    constructor(props){
        super(props);

        this.state = {
            value: '',
            matches: [],
            showMatches: false

        }

    }

    searchListing(){
        api.call {
           that.setState({
                showMatches: true, 
                matches: a
            });
        })
        }

    }

    handleOnChangeInput(e){

        let value = e.target.value;
        this.setState({ value: value})

        if(value !== ''){
            this.searchListing(e);
        }else{
            // console.log("value", e.target.value);          
            this.setState({
                showMatches: false,
                matches: []
            })
        }
    }

    render(){

        let matches = this.state.matches;
        let value = this.state.value;
        let matchesHtml;

        if(this.state.showMatches){
            matchesHtml = <ul>
                            {matches.map(function(match, idx){
                                let re = new RegExp(value, 'g');
                                let str = match.replace(re, '<b>'+ value +'</b>');
                                return <li key={idx} dangerouslySetInnerHTML={{__html: str}}></li>

                            })}
                        </ul>
        }
        return(
            <div>
               <input placeholder="type a name" onChange={this.handleOnChangeInput}/>

               {matchesHtml}
            </div>
        );
    }
}

I have a custom autoplete, 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 the same as the input value.

So if I have a list of suggestions: "alligator", "lima", "lime", and I typed "li", then the suggestions would look like this:

  • alligator
  • lima
  • lime

I have this simple map in my jsx file:

<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>

where value is the input value. It displays the list but in this string format

  • al<b>li</b>gator
  • <b>li</b>ma
  • <b>li</b>me

Not sure how to go about with React. I thought of using dangerouslyinnerhtml or something like that, but I think that's a last resort thing. I would like to avoid that if possible.

This my autoplete ponent:

class Autoplete extends Component{

    constructor(props){
        super(props);

        this.state = {
            value: '',
            matches: [],
            showMatches: false

        }

    }

    searchListing(){
        api.call {
           that.setState({
                showMatches: true, 
                matches: a
            });
        })
        }

    }

    handleOnChangeInput(e){

        let value = e.target.value;
        this.setState({ value: value})

        if(value !== ''){
            this.searchListing(e);
        }else{
            // console.log("value", e.target.value);          
            this.setState({
                showMatches: false,
                matches: []
            })
        }
    }

    render(){

        let matches = this.state.matches;
        let value = this.state.value;
        let matchesHtml;

        if(this.state.showMatches){
            matchesHtml = <ul>
                            {matches.map(function(match, idx){
                                let re = new RegExp(value, 'g');
                                let str = match.replace(re, '<b>'+ value +'</b>');
                                return <li key={idx} dangerouslySetInnerHTML={{__html: str}}></li>

                            })}
                        </ul>
        }
        return(
            <div>
               <input placeholder="type a name" onChange={this.handleOnChangeInput}/>

               {matchesHtml}
            </div>
        );
    }
}
Share Improve this question edited Aug 21, 2019 at 21:07 Emile Bergeron 17.4k5 gold badges85 silver badges131 bronze badges asked Aug 21, 2019 at 18:42 medev21medev21 3,05110 gold badges35 silver badges51 bronze badges 1
  • it would be the same in react, how are you showing auto-populated data in your react? You can just attach this ui > li's as children in your ponent – Rikin Commented Aug 21, 2019 at 18:46
Add a ment  | 

3 Answers 3

Reset to default 8

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.

A React ponent can also return an array of elements:

render() {
  // No need to wrap list items in an extra element!
  return [
    // Don't forget the keys :)
    <li key="A">First item</li>,
    <li key="B">Second item</li>,
    <li key="C">Third item</li>,
  ];
}

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 ponent 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./ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare./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 ponent

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

/**
 * React ponent 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>

Just use dangerouslySetInnerHTML but take attention it make to inadvertently expose your users to a cross-site scripting (XSS) attack

...
const valueToBold = (match: string) => {
   const regex = new RegExp(searchFilterValue, 'g');
   return match.replace(regex, '<b>$&</b>');
};
return (
...
 <ul>
   {matches.map((match, idx)=> ( 
     <li key={idx}>
      <span dangerouslySetInnerHTML={{ __html:valueToBold(match) }} />
     </li>
    ))}
 </ul>
...
)

You just append your mapper as children inside your auto plete ponent.

<CustomAutoplete>
  <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>
</CustomAutoplete>
发布评论

评论列表(0)

  1. 暂无评论