Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to highlight matches within a string with JSX?

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 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 autocomplete component:

class Autocomplete 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>
        );
    }
}
like image 568
medev21 Avatar asked Aug 21 '19 18:08

medev21


People also ask

How do you highlight text in react JS?

To highlight text using React, we can create our own component where we check whether each word matches what we want to highlight. to create the Highlighted component to highlight the text set as the highlight option.

What is JSX element []?

What is JSX? JSX stands for JavaScript XML. JSX allows us to write HTML in React. JSX makes it easier to write and add HTML in React.

How do you highlight a string in JavaScript?

Highlight Text Using the Mark Tag Method in JavaScriptIf you surround any text inside the mark tag, the browser will automatically highlight it in a striking yellow color. This will make highlighting a searched text quite a simple task then.

How do you use JavaScript expressions inside JSX?

You can embed any JavaScript expression in JSX by wrapping it in curly braces. For example, 2 + 2 , user. firstName , and formatName(user) are all valid expressions.


1 Answers

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 component 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 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>
like image 137
Emile Bergeron Avatar answered Nov 14 '22 05:11

Emile Bergeron