51

I'm trying to highlight text matching the query but I can't figure out how to get the tags to display as HTML instead of text.

var Component = React.createClass({
    _highlightQuery: function(name, query) {
        var regex = new RegExp("(" + query + ")", "gi");
        return name.replace(regex, "<strong>$1</strong>");
    },
    render: function() {
        var name = "Javascript";
        var query = "java"
        return (
            <div>
                <input type="checkbox" /> {this._highlightQuery(name, query)}
            </div>
        );
    }
});

Current Output: <strong>Java</strong>script

Desired Output: Javascript

Andrew Hunt
  • 554
  • 1
  • 4
  • 11

15 Answers15

130

Here is my simple twoliner helper method:

getHighlightedText(text, highlight) {
    // Split text on highlight term, include term itself into parts, ignore case
    const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
    return <span>{parts.map(part => part.toLowerCase() === highlight.toLowerCase() ? <b>{part}</b> : part)}</span>;
}

It returns a span, where the requested parts are highlighted with <b> </b> tags. This can be simply modified to use another tag if needed.

UPDATE: To avoid unique key missing warning, here is a solution based on spans and setting fontWeight style for matching parts:

getHighlightedText(text, highlight) {
    // Split on highlight term and include term into parts, ignore case
    const parts = text.split(new RegExp(`(${highlight})`, 'gi'));
    return <span> { parts.map((part, i) => 
        <span key={i} style={part.toLowerCase() === highlight.toLowerCase() ? { fontWeight: 'bold' } : {} }>
            { part }
        </span>)
    } </span>;
}
Tom
  • 2,543
  • 3
  • 21
  • 42
peter.bartos
  • 11,855
  • 3
  • 51
  • 62
  • This tutorial might help someone too: https://www.vladopandzic.com/react/creating-react-highlighter-component/ – Vlado Pandžić Sep 13 '17 at 10:23
  • 9
    I would like to point something out. I was confused about why his "parts" still included the separator. Think about it, string.split() always excludes the separator. Turns out, there is a super special case where .split( regex ), where the regex has a capturing group, will also insert the captured elements into the resulting array. Here is a link to the official docs about this super random edge case: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split . Scroll down to "Splitting with a RegExp to include parts of the separator in the result" – Captainlonate Aug 02 '18 at 21:37
  • Based on some experiments, I think the matching parts will always be at odd indices. So you can test for a match with `index % 2 === 1` instead of `part.toLowerCase() === higlight.toLowerCase()`. This comes in handy when you have multiple highlight words, and it's more of a hassle to compare with an array every time. Sample experiment: `"fooAfoobar".split(/(foo|bar)/gi)` -> `[ '', 'foo', 'A', 'foo', '', 'bar', '' ]` – Chris Aug 16 '19 at 09:27
  • nitpick: you should use ```const parts = text.split(new RegExp(`(${higlight})`, 'gi'));``` instead of ```let parts = text.split(new RegExp(`(${higlight})`, 'gi'));``` – P A S H Jan 22 '20 at 02:35
  • 1
    Thanks you! Its good! I take your function and write use his functional component React. https://codesandbox.io/s/musing-rgb-4jkpi?file=/src/HightLigthter.jsx:68-88 – Aleksandr Zelenskiy Apr 23 '21 at 15:38
28

Here is an example of a react component that uses the standard <mark> tag to highlight a text:

const Highlighted = ({text = '', highlight = ''}) => {
   if (!highlight.trim()) {
     return <span>{text}</span>
   }
   const regex = new RegExp(`(${_.escapeRegExp(highlight)})`, 'gi')
   const parts = text.split(regex)
   return (
     <span>
        {parts.filter(part => part).map((part, i) => (
            regex.test(part) ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
        ))}
    </span>
   )
}

And here is how to use it

<Highlighted text="the quick brown fox jumps over the lazy dog" highlight="fox"/>
Steve
  • 25,806
  • 2
  • 33
  • 43
Henok T
  • 1,014
  • 13
  • 7
10

There is already a react component on NPM to do what you want:

var Highlight = require('react-highlighter');
[...]
<Highlight search={regex}>{name}</Highlight>
aij
  • 5,903
  • 3
  • 37
  • 41
8

Here's my solution.

I tried to focus on simplicity and performance, so I avoided solutions that involved manual manipulation of the DOM outside of React, or unsafe methods like dangerouslySetInnerHTML.

Additionally, this solution takes care of combining subsequent matches into a single <span/>, thus avoiding having redundant spans.

const Highlighter = ({children, highlight}) => {
  if (!highlight) return children;
  const regexp = new RegExp(highlight, 'g');
  const matches = children.match(regexp);
  console.log(matches, parts);
  var parts = children.split(new RegExp(`${highlight.replace()}`, 'g'));

  for (var i = 0; i < parts.length; i++) {
    if (i !== parts.length - 1) {
      let match = matches[i];
      // While the next part is an empty string, merge the corresponding match with the current
      // match into a single <span/> to avoid consequent spans with nothing between them.
      while(parts[i + 1] === '') {
        match += matches[++i];
      }

      parts[i] = (
        <React.Fragment key={i}>
          {parts[i]}<span className="highlighted">{match}</span>
        </React.Fragment>
      );
    }
  }
  return <div className="highlighter">{parts}</div>;
};

Usage:

<Highlighter highlight='text'>Some text to be highlighted</Highlighter>

Check out this codepen for a live example.

Yoav Kadosh
  • 4,807
  • 4
  • 39
  • 56
4

By default ReactJS escapes HTML to prevent XSS. If you do wish to set HTML you need to use the special attribute dangerouslySetInnerHTML. Try the following code:

render: function() {
        var name = "Javascript";
        var query = "java"
        return (
            <div>
                <input type="checkbox" /> <span dangerouslySetInnerHTML={{__html: this._highlightQuery(name, query)}}></span>
            </div>
        );
    }
Yanik Ceulemans
  • 1,182
  • 9
  • 17
  • 1
    Is there a better way to do it without using the dangerouslySetInnerHTML attribute? Thanks – Andrew Hunt Apr 15 '15 at 14:37
  • 4
    Yes there is. This would require creating a new React component that handles the 'highlighting' of the query. You could then use that component inside of this one. This approach would be more in the spirit of ReactJS where things are broken down into small components, each doing 1 thing and doing it really well. This is the [single responsibility principle](http://en.wikipedia.org/wiki/Single_responsibility_principle) and it applies to other areas of programming as well. – Yanik Ceulemans Apr 16 '15 at 07:38
4
  const escapeRegExp = (str = '') => (
    str.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')
  );

  const Highlight = ({ search = '', children = '' }) => {
    const patt = new RegExp(`(${escapeRegExp(search)})`, 'i');
    const parts = String(children).split(patt);

    if (search) {
      return parts.map((part, index) => (
        patt.test(part) ? <mark key={index}>{part}</mark> : part
      ));
    } else {
      return children;
    }
  };

  <Highlight search="la">La La Land</Highlight>
3

With react-mark.js you can simply:

<Marker mark="hello">
  Hello World
</Marker>

Links:

Dharman
  • 30,962
  • 25
  • 85
  • 135
Aakash
  • 21,375
  • 7
  • 100
  • 81
2

Mark matches as a function https://codesandbox.io/s/pensive-diffie-nwwxe?file=/src/App.js

import React from "react";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      res: "Lorem ipsum dolor"
    };
    this.markMatches = this.markMatches.bind(this);
  }
  markMatches(ev) {
    let res = "Lorem ipsum dolor";
    const req = ev.target.value;
    if (req) {
      const normReq = req
        .toLowerCase()
        .replace(/\s+/g, " ")
        .trim()
        .split(" ")
        .sort((a, b) => b.length - a.length);
      res = res.replace(
        new RegExp(`(${normReq.join("|")})`, "gi"),
        match => "<mark>" + match + "</mark>"
      );
    }
    this.setState({
      res: res
    });
  }

  render() {
    return (
      <div className="App">
        <input type="text" onChange={this.markMatches} />
        <br />
        <p dangerouslySetInnerHTML={{ __html: this.state.res }} />
      </div>
    );
  }
}

export default App;
user3713526
  • 435
  • 7
  • 16
  • This is my favourite solution, it not only solves the OP's request, elegantly, but also allows for multiple matches being replaced simultaneously, nice one! – Morgan Feeney Jan 10 '21 at 10:08
2

Based on @Henok T's solution, here is one without lodash.

It is implement in Typescript and uses Styled-components, but can be easily adapted to vanilla JS, by simply removing the types and adding the styles inline.

import React, { useMemo } from "react";
import styled from "styled-components";

const MarkedText = styled.mark`
  background-color: #ffd580;
`;

interface IHighlighted { 
  text?: string;
  search?: string;
}

export default function Highlighted({ text = "", search = "" }: IHighlighted): JSX.Element {
  /**
   * The brackets around the re variable keeps it in the array when splitting and does not affect testing
   * @example 'react'.split(/(ac)/gi) => ['re', 'ac', 't']
   */
  const re = useMemo(() => {
    const SPECIAL_CHAR_RE = /([.?*+^$[\]\\(){}|-])/g;
    const escapedSearch = search.replace(SPECIAL_CHAR_RE, "\\$1");
    return new RegExp(`(${escapedSearch})`, "i");
  }, [search]);

  return (
    <span>
      {search === ""
        ? text
        : text
            .split(re)
            .filter((part) => part !== "")
            .map((part, i) => (re.test(part) ? <MarkedText key={part + i}>{part}</MarkedText> : part))}
    </span>
  );
}
lbragile
  • 7,549
  • 3
  • 27
  • 64
2
const highlightMatchingText = (text, matchingText) => {
  const matchRegex = RegExp(matchingText, 'ig');

  // Matches array needed to maintain the correct letter casing
  const matches = [...text.matchAll(matchRegex)];

  return text
    .split(matchRegex)
    .map((nonBoldText, index, arr) => (
      <React.Fragment key={index}>
        {nonBoldText}
        {index + 1 !== arr.length && <mark>{matches[index]}</mark>}
      </React.Fragment>
  ));
};

How to use it:

<p>highlightMatchingText('text here', 'text')</p>

or

<YourComponent text={highlightMatchingText('text here', 'text')}/>
Peter Rhodes
  • 698
  • 6
  • 14
0

This should work:

var Component = React.createClass({
    _highlightQuery: function(name, query) {
        var regex = new RegExp("(" + query + ")", "gi");
        return "<span>"+name.replace(regex, "<strong>$1</strong>")+"</span>";
    },
    render: function() {
        var name = "Javascript";
        var query = "java"
        return (
            <div>
                <input type="checkbox" />{JSXTransformer.exec(this._highlightQuery(name, query))}
            </div>
        );
    }
});

Basically you're generating a react component on the fly. If you want, you can put the <span> tag inside the render() function rather then the _highlightQuery() one.

Cristik
  • 30,989
  • 25
  • 91
  • 127
0

I would suggest you use a different approach. Create one component, say <TextContainer />, which contains <Text /> elements.

var React = require('react');
var Text = require('Text.jsx');

var TextContainer = React.createClass({
    getInitialState: function() {
        return {
            query: ''
        };
    },
    render: function() {
        var names = this.props.names.map(function (name) {
            return <Text name={name} query={this.state.query} />
        });
        return (
            <div>
                {names}
           </div>
        );
    }
});

module.exports = TextContainer;

As you see the text container holds as state the current query. Now, the <Text /> component could be something like this:

var React = require('react');

var Text = React.createClass({
    propTypes: {
        name: React.PropTypes.string.isRequired,
        query: React.PropTypes.string.isRequired
    },

    render: function() {
        var query = this.props.query;
        var regex = new RegExp("(" + query + ")", "gi");
        var name = this.props.name;
        var parts = name.split(regex);
        var result = name;

        if (parts) {
            if (parts.length === 2) {
                result =
                    <span>{parts[0]}<strong>{query}</strong>{parts[1]}</span>;
            } else {
                if (name.search(regex) === 0) {
                    result = <span><strong>{query}</strong>{parts[0]}</span>
                } else {
                    result = <span>{query}<strong>{parts[0]}</strong></span>
                }
            }
        }

        return <span>{result}</span>;
    }

});

module.exports = Text;

So, the root component has as state, the current query. When its state will be changed, it will trigger the children's render() method. Each child will receive the new query as a new prop, and output the text, highlighting those parts that would match the query.

Todd
  • 3,438
  • 1
  • 27
  • 36
gcedo
  • 4,811
  • 1
  • 21
  • 28
0

I had the requirement to search among the comments contain the HTML tags.

eg: One of my comments looks like below example

Hello World

<div>Hello<strong>World</strong></div>

So, I wanted to search among all these kinds of comments and highlight the search result.

As we all know we can highlight text using HTML tag <mark>

So. I have created one helper function which performs the task of adding <mark> tag in the text if it contains the searched text.

getHighlightedText = (text, highlight) => {
    if (!highlight.trim()) {
      return text;
    }
    const regex = new RegExp(`(${highlight})`, "gi");
    const parts = text.split(regex);
    const updatedParts = parts
      .filter((part) => part)
      .map((part, i) =>
        regex.test(part) ? <mark key={i}>{part}</mark> : part
      );
    let newText = "";
    [...updatedParts].map(
      (parts) =>
        (newText =
          newText +
          (typeof parts === "object"
            ? `<${parts["type"]}>${highlight}</${parts["type"]}>`
            : parts))
    );
    return newText;
  };

So, We have to pass our text and search text inside the function as arguments.

Input

getHighlightedText("<div>Hello<strong>World</strong></div>", "hello")

Output

<div><mark>Hello</mark><strong>World</strong></div>

Let me know if need more help with solutions.

Krunal Rajkotiya
  • 1,070
  • 9
  • 14
  • what if I want "oW" to match the last letter of "Hello" and first letter of "World"? is that possible? – Dror Bar Apr 13 '22 at 15:06
  • @DrorBar Yes, it's possible but in that case, you have to make a list of the characters of the search string in your case it will be ow ==> ['o', 'w'] then search all your data which consists of these letters to satisfy your requirements. – Krunal Rajkotiya Apr 27 '22 at 09:09
0

I have extended the version from @Henok T from above to be able to highlight multiple text parts splitted by space but keep strings in quotes or double quotes together.

e.g. a highlight of text "some text" 'some other text' text2 would highlight the texts:

text some text some other text text2 in the given text.

 const Highlighted = ({text = '', highlight = ''}: { text: string; highlight: string; }) => {
    if (!highlight.trim()) {
        return <span>{text}</span>
    }
  
    var highlightRegex = /'([^']*)'|"([^"]*)"|(\S+)/gi;  // search for all strings but keep strings with "" or '' together
    var highlightArray = (highlight.match(highlightRegex) || []).map(m => m.replace(highlightRegex, '$1$2$3'));

    // join the escaped parts with | to a string
    const regexpPart= highlightArray.map((a) => `${_.escapeRegExp(a)}`).join('|');
    
    // add the regular expression
    const regex = new RegExp(`(${regexpPart})`, 'gi')
   
    const parts = text.split(regex)
    return (
        <span>
            {parts.filter(part => part).map((part, i) => (
                regex.test(part) ? <mark key={i}>{part}</mark> : <span key={i}>{part}</span>
            ))}
        </span>
    )
}
BHoft
  • 1,663
  • 11
  • 18
0
import React from 'react';
//cm_chandan_
export default function App() {
  const getSelection = () => {
    if (window.getSelection().focusNode) {
      var sel = window.getSelection();
      var range = sel.getRangeAt(0);
      if (
        sel.rangeCount &&
        range.startContainer.isSameNode(range.endContainer)
      ) {
        var selectionAnchorOffset = sel.anchorOffset;
        var selectionFocusffset = sel.focusOffset;
        if (selectionAnchorOffset > selectionFocusffset) {
          selectionAnchorOffset = sel.focusOffset;
          selectionFocusffset = sel.anchorOffset;
        }
        let parentNodeSelection = sel.anchorNode.parentNode;
        let childNodesArr = parentNodeSelection.childNodes;
        for (let i = 0; i < childNodesArr.length; i++) {
          if (childNodesArr[i].nodeType === 3) {
            if (childNodesArr[i].isEqualNode(sel.anchorNode)) {
              let contentNodeText = childNodesArr[i].textContent;
              let startTextNode = document.createTextNode(
                contentNodeText.slice(0, selectionAnchorOffset)
              );
              let endTextNode = document.createTextNode(
                contentNodeText.slice(
                  selectionFocusffset,
                  contentNodeText.length
                )
              );
              let highlightSpan = document.createElement('span');
              highlightSpan.innerHTML = sel.toString();
              highlightSpan.style.background = 'red';
              childNodesArr[i].after(endTextNode);
              childNodesArr[i].after(highlightSpan);
              childNodesArr[i].after(startTextNode);
              childNodesArr[i].remove();
            }
          }
        }
      } else {
        alert('Wrong Text Select!');
      }
    } else {
      alert('Please Text Select!');
    }
  };
  return (
    <>
      <p id="textHilight">
        <span id="heeader">
          <b>
            Organization’s vision An organization’s vision for a privacy program
            needs to include data protection as well as data usage functions.
          </b>
        </span>

        <br />

        <span id="desc">
          To be effective, the privacy program vision may also need to include
          IT governance if this is lacking. Executive sponsorship is the formal
          or informal approval to commit resources to a business problem or
          challenge Privacy is no exception: without executive sponsorship,
          privacy would be little more than an idea.As vision gives way to
          strategy, the organization’s privacy leader must ensure that the
          information privacy program fits in with the rest of the organization.
          idea
        </span>
      </p>
      <button
        onClick={() => {
          getSelection();
        }}
      >
        click
      </button>
    </>
  );
}