Highlighting a filtered result in AngularJS

I'm using a ng-repeat and filter in angularJS like the phones tutorial but I'd like to highlight the search results in the page. With basic jQuery I would have simply parsed the page on key up on the input, but I'm trying to do it the angular way. Any ideas ?

My code :

<input id="search" type="text" placeholder="Recherche DCI" ng-model="search_query" autofocus>
<tr ng-repeat="dci in dcis | filter:search_query">
            <td class='marque'>{{dci.marque}} ®</td>
            <td class="dci">{{dci.dci}}</td>
 </tr>

In did that for AngularJS v1.2+

HTML:

<span ng-bind-html="highlight(textToSearchThrough, searchText)"></span>

JS:

$scope.highlight = function(text, search) {
    if (!search) {
        return $sce.trustAsHtml(text);
    }
    return $sce.trustAsHtml(text.replace(new RegExp(search, 'gi'), '<span class="highlightedText">$&</span>'));
};

CSS:

.highlightedText {
    background: yellow;
}

Try Angular UI

Filters -> Highlite (filter). There is also Keypress directive.

angular ui-utils supports only one term. I'm using the following filter rather than a scope function:

app.filter('highlight', function($sce) {
  return function(str, termsToHighlight) {
    // Sort terms by length
    termsToHighlight.sort(function(a, b) {
      return b.length - a.length;
    });
    // Regex to simultaneously replace terms
    var regex = new RegExp('(' + termsToHighlight.join('|') + ')', 'g');
    return $sce.trustAsHtml(str.replace(regex, '<span class="match">$&</span>'));
  };
});

And the HTML:

<span ng-bind-html="theText | highlight:theTerms"></span>

index.html

<!DOCTYPE html>
<html>
  <head>
    <script src="angular.js"></script>
    <script src="app.js"></script>
    <style>
      .highlighted { background: yellow }
    </style>
  </head>

  <body ng-app="Demo">
    <h1>Highlight text using AngularJS.</h1>

    <div class="container" ng-controller="Demo">
      <input type="text" placeholder="Search" ng-model="search.text">

      <ul>
        <!-- filter code -->
        <div ng-repeat="item in data | filter:search.text"
           ng-bind-html="item.text | highlight:search.text">
        </div>
      </ul>
    </div>
  </body>
</html>

app.js

angular.module('Demo', [])
  .controller('Demo', function($scope) {
    $scope.data = [
      { text: "<< ==== Put text to Search ===== >>" }
    ]
  })
  .filter('highlight', function($sce) {
    return function(text, phrase) {
      if (phrase) text = text.replace(new RegExp('('+phrase+')', 'gi'),
        '<span class="highlighted">$1</span>')

      return $sce.trustAsHtml(text)
    }
  })

Reference : http://codeforgeek.com/2014/12/highlight-search-result-angular-filter/ demo : http://demo.codeforgeek.com/highlight-angular/

Use ng-class that is applied when the search term is related to the data the element contains.

So on your ng-repeated elements, you'd have ng-class="{ className: search_query==elementRelatedValue}"

which would apply class "className" to elements dynamically when the condition is met.

About the problems with special caracter, I think just escaping you might lose regex search.

What about this:

function(text, search) {
    if (!search || (search && search.length < 3)) {
        return $sce.trustAsHtml(text);
    }

    regexp  = '';

    try {
        regexp = new RegExp(search, 'gi');
    } catch(e) {
        return $sce.trustAsHtml(text);
    }

    return $sce.trustAsHtml(text.replace(regexp, '<span class="highlight">$&</span>'));
};

An invalid regexp could be user just typing the text:

  • valid: m
  • invalid: m[
  • invalid: m[ô
  • invalid: m[ôo
  • valid: m[ôo]
  • valid: m[ôo]n
  • valid: m[ôo]ni
  • valid: m[ôo]nic
  • valid: m[ôo]nica

What do you think @Mik Cox?

Another proposition:

app.filter('wrapText', wrapText);

function wrapText($sce) {
    return function (source, needle, wrap) {
        var regex;

        if (typeof needle === 'string') {
            regex = new RegExp(needle, "gi");
        } else {
            regex = needle;
        }

        if (source.match(regex)) {
            source = source.replace(regex, function (match) {
                return $('<i></i>').append($(wrap).text(match)).html();
            });
        }

        return $sce.trustAsHtml(source);
    };
} // wrapText

wrapText.$inject = ['$sce'];

// use like this
$filter('wrapText')('This is a word, really!', 'word', '<span class="highlight"></span>');
// or like this
{{ 'This is a word, really!' | wrapText:'word':'<span class="highlight"></span>' }}

I'm open to criticism ! ;-)

Thanks for asking this as it was something I was dealing with as well.

Two things though:

First, The top answer is great but the comment on it is accurate that highlight() has problem with special characters. That comment suggests using an escaping chain which will work but they suggest using unescape() which is being phased out. What I ended up with:

$sce.trustAsHtml(decodeURI(escape(text).replace(new RegExp(escape(search), 'gi'), '<span class="highlightedText">$&</span>')));

Second, I was trying to do this in a data bound list of URLs. While in the highlight() string, you don't need to data bind.

Example:

<li>{{item.headers.host}}{{item.url}}</li>

Became:

<span ng-bind-html="highlight(item.headers.host+item.url, item.match)"></span>

Was running into problems with leaving them in {{ }} and getting all sorts of errors.

Hope this helps anybody running into the same problems.

Going off of @uri's answer in this thread, I modified it to work with a single string OR a string array.

Here's the TypeScript version

module myApp.Filters.Highlight {
    "use strict";

    class HighlightFilter {
        //This will wrap matching search terms with an element to visually highlight strings
        //Usage: {{fullString | highlight:'partial string'}}
        //Usage: {{fullString | highlight:['partial', 'string, 'example']}}

        static $inject = ["$sce"];

        constructor($sce: angular.ISCEService) {

            // The `terms` could be a string, or an array of strings, so we have to use the `any` type here
            /* tslint:disable: no-any */
            return (str: string, terms: any) => {
                /* tslint:enable */

                if (terms) {
                    var allTermsRegexStr: string;

                    if (typeof terms === "string") {
                        allTermsRegexStr = terms;
                    } else { //assume a string array
                        // Sort array by length then  join with regex pipe separator
                        allTermsRegexStr = terms.sort((a: string, b: string) => b.length - a.length).join('|');
                    }

                    // Regex to simultaneously replace terms - case insensitive!
                    var regex = new RegExp('(' + allTermsRegexStr + ')', 'ig');

                    return $sce.trustAsHtml(str.replace(regex, '<mark class="highlight">$&</mark>'));
                } else {
                    return str;
                }
            };
        }
    }

    angular
        .module("myApp")
        .filter("highlight", HighlightFilter);
};

Which translates to this in JavaScript:

var myApp;
(function (myApp) {
    var Filters;
    (function (Filters) {
        var Highlight;
        (function (Highlight) {
            "use strict";
            var HighlightFilter = (function () {
                function HighlightFilter($sce) {
                    // The `terms` could be a string, or an array of strings, so we have to use the `any` type here
                    /* tslint:disable: no-any */
                    return function (str, terms) {
                        /* tslint:enable */
                        if (terms) {
                            var allTermsRegexStr;
                            if (typeof terms === "string") {
                                allTermsRegexStr = terms;
                            }
                            else {
                                // Sort array by length then  join with regex pipe separator
                                allTermsRegexStr = terms.sort(function (a, b) { return b.length - a.length; }).join('|');
                            }
                            // Regex to simultaneously replace terms - case insensitive!
                            var regex = new RegExp('(' + allTermsRegexStr + ')', 'ig');
                            return $sce.trustAsHtml(str.replace(regex, '<mark class="highlight">$&</mark>'));
                        }
                        else {
                            return str;
                        }
                    };
                }
                //This will wrap matching search terms with an element to visually highlight strings
                //Usage: {{fullString | highlight:'partial string'}}
                //Usage: {{fullString | highlight:['partial', 'string, 'example']}}
                HighlightFilter.$inject = ["$sce"];
                return HighlightFilter;
            })();
            angular.module("myApp").filter("highlight", HighlightFilter);
        })(Highlight = Filters.Highlight || (Filters.Highlight = {}));
    })(Filters = myApp.Filters || (myApp.Filters = {}));
})(myApp|| (myApp= {}));
;

Or if you just want a simple JavaScript implementation without those generated namespaces:

app.filter('highlight', ['$sce', function($sce) {
    return function (str, terms) {
        if (terms) {
            var allTermsRegexStr;
            if (typeof terms === "string") {
                allTermsRegexStr = terms;
            }
            else {
                // Sort array by length then  join with regex pipe separator
                allTermsRegexStr = terms.sort(function (a, b) { return b.length - a.length; }).join('|');
            }
            // Regex to simultaneously replace terms - case insensitive!
            var regex = new RegExp('(' + allTermsRegexStr + ')', 'ig');
            return $sce.trustAsHtml(str.replace(regex, '<mark class="highlight">$&</mark>'));
        }
        else {
            return str;
        }
    };
}]);