58

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>
Lukmo
  • 1,688
  • 5
  • 20
  • 31
  • You can register a keyup listener as [given here](http://stackoverflow.com/questions/11264188/how-can-i-detect-onkeyup-in-angularjs) – Arun P Johny Mar 20 '13 at 09:34

13 Answers13

95

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;
}
Dmitri Algazin
  • 3,332
  • 27
  • 30
  • 10
    It should be noted that the above has problems with special characters, like matching ( or &, etc. I've solved this issue by escape()-ing both the search and text strings, and then returning the unescape() version of the $sce.trustAsHtml, which seems to handle special characters properly: `return $sce.trustAsHtml(unescape(escape(text).replace(new RegExp(escape(search), 'gi'), '$&')));` – Mik Cox May 13 '14 at 17:18
  • 2
    What if the search text appears in some link? Such as to search `apple` and the returning html contains `http://www.apple.com`. Won't it break the link? – wang zhihao Jan 22 '15 at 07:35
  • 2
    You should escape special characters with this function `RegExp.quote = function(str) { return (str+'').replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); };` Like this: `new RegExp(RegExp.quote(search), 'gi')` – cnmuc Apr 15 '15 at 06:56
  • To elaborate on Mik Cox's comment; since "escape" and "unescape" are deprecated, these can be replaced by "encodeURI" and "decodeURI" respectively, for simple escaping. – kvdv May 29 '15 at 08:23
  • can you please explain what does `gi` and `$&` means... please – Vikas Bansal Nov 28 '16 at 06:51
  • It should also be noted that that his solution has problems when the search term appears as an element name, attribute name/value, or (as @wangzhihao mentioned) URIs, assuming the text contains HTML already. – c1moore Apr 11 '17 at 20:24
  • @VikasBansal `gi` are [regex flags](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp). `g` means use a global search (don't just stop at the first instance) and `i` means case-insensitive. [`$&`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastMatch) is like a variable that will be replaced with the last matched term. – c1moore Apr 11 '17 at 20:57
  • WARNING: please do not use this for anything that could accept user input as you'd be exposing your users to HTML injection attacks! You need to first sanitize any user data. – Zane Oct 09 '19 at 03:56
22

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>
Uri
  • 25,622
  • 10
  • 45
  • 72
  • 1
    I get an error that ` TypeError: undefined is not a function` when using this... If I console out the str and termsToHighlight they are both strings. – Cameron Feb 03 '15 at 17:30
  • 1
    `theTerms` should be an array of strings. – Uri Feb 03 '15 at 17:37
  • 2
    I had to fix it by first doing this: `termsToHighlight = termsToHighlight.split(" ");` before the sort. – Cameron Feb 03 '15 at 17:39
  • Also it doesn't work for case, so 'lorem' doesn't match 'Lorem' – Cameron Feb 03 '15 at 17:40
  • 3
    So I did `var regex = new RegExp('(' + termsToHighlight.join('|') + ')', 'ig');` – Cameron Feb 03 '15 at 17:54
  • See my answer below, but this will likely break if a search is performed for `.` or some other character that has meaning in a regular expression. – Chris Barr Nov 05 '15 at 15:14
  • In this case dot will apply the filter to the whole of the terms. Anyway, I came here to say thanks, this had helped me. – Alex Szabo Dec 22 '15 at 12:45
13

Try Angular UI

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

Dmitriy
  • 1,422
  • 15
  • 27
  • 1
    AFAIK this works just for one term. If you need more than one at the same time you need a different solution. – Uri Aug 17 '14 at 09:07
  • can you link to page? i can 't find it – chovy Nov 09 '14 at 22:36
  • 5
    The highlight filter is no longer available, it has been deprecated. Go to https://github.com/angular-ui/ui-utils to use the deprecated code. – fauverism Jul 27 '15 at 17:43
9

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/

Shaikh Shahid
  • 1,185
  • 11
  • 13
6

I hope my light example will make it easy to understand:

  app.filter('highlight', function() {
    return function(text, phrase) {
      return phrase 
        ? text.replace(new RegExp('('+phrase+')', 'gi'), '<kbd>$1</kbd>') 
        : text;
    };
  });

  <input type="text" ng-model="search.$">

  <ul>
    <li ng-repeat="item in items | filter:search">
      <div ng-bind-html="item | highlight:search.$"></div>
    </li>
  </ul>

enter image description here

mrded
  • 4,674
  • 2
  • 34
  • 36
  • what is `phrase` and `text` here? – xkeshav Mar 23 '17 at 06:23
  • This looks nice, and I'm trying to work it out. First attempts end with "angular.js:14516 Error: [$injector:unpr]". So I'm trying to figure out if I must inject the filter somewhere. No luck by now. Any clue? – mayid Apr 10 '17 at 16:45
  • Ok, worked for me returning a function in the filter. And not using $. Also I had to secure the html with $sce. – mayid Apr 10 '17 at 18:28
  • Guys, can you please stop adding `$sce` into my example. It's just an example, it must be simple to understand. If you need - wrap it up in your code. Thank you. – mrded Jun 08 '17 at 14:42
5

There is standart Highlight filter in the angular-bootstrap: typeaheadHighlight

Usage

<span ng-bind-html="text | typeaheadHighlight:query"></span>

With scope {text:"Hello world", query:"world"} renders in

<span...>Hello <strong>world</strong></span>
user854301
  • 5,383
  • 3
  • 28
  • 37
4

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) {
                    let 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('|');
                    }

                //Escape characters that have meaning in regular expressions
                //via: http://stackoverflow.com/a/6969486/79677
                allTermsRegexStr = allTermsRegexStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");

                    // 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('|');
                            }

                            //Escape characters that have meaning in regular expressions
                            //via: http://stackoverflow.com/a/6969486/79677
                            allTermsRegexStr = allTermsRegexStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");

                            // 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('|');
            }

            //Escape characters that have meaning in regular expressions
            //via: http://stackoverflow.com/a/6969486/79677
            allTermsRegexStr = allTermsRegexStr.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");

            // 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;
        }
    };
}]);

EDITED to include a fix that would have previously broken this is someone searched for . or any other character that had meaning in a regular expression. Now those characters get escaped first.

Chris Barr
  • 29,851
  • 23
  • 95
  • 135
  • 1
    Escaping `|` will brake the regex. It' should be probably something like this `var escape = new RegExp('[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\\^\\$\\|]', 'g'); var termsEscaped : string [] = new Array(); terms.sort((a: string, b: string) => b.length - a.length).forEach( (value:string, index: number, array : string[]) : void => { termsEscaped.push(value.replace(escape, "\\$&")); }); var allTermsRegexStr : string = termsEscaped.join('|');` – Fabian Jul 12 '16 at 13:24
1

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.

holographic-principle
  • 19,688
  • 10
  • 46
  • 62
  • I have to ship quite fast so I'll end up using the angularUI filter but I'll try to understand your answer this weekend since it'll grant me a better understanding of the angular way. Thanks ! – Lukmo Mar 20 '13 at 09:56
1

My solution for highlight, used this with angular-ui-tree element: https://codepen.io/shnigi/pen/jKeaYG

angular.module('myApp').filter('highlightFilter', $sce =>
 function (element, searchInput) {
   element = element.replace(new RegExp(`(${searchInput})`, 'gi'),
             '<span class="highlighted">$&</span>');
   return $sce.trustAsHtml(element);
 });

Add css:

.highlighted {
  color: orange;
}

HTML:

<p ng-repeat="person in persons | filter:search.value">
  <span ng-bind-html="person | highlightFilter:search.value"></span>
</p>

And to add search input:

<input type="search" ng-model="search.value">
Shnigi
  • 972
  • 9
  • 19
0

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?

Andre Medeiros
  • 224
  • 2
  • 6
0

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 ! ;-)

Monkey Monk
  • 984
  • 9
  • 19
0

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.

mattjay
  • 1
  • 1
  • I'm having all sorts of trouble getting this to work. What does ", item.match" correspond to in your code? – Rich Finelli Dec 15 '14 at 20:02
  • 1
    Sorry, @richfinelli, that is confusing on my part. This is part of an ng-repeated table. – mattjay Dec 16 '14 at 21:21
  • Okay so I can probably leave that off in my case. All I probably need is something like... ng-bind-html="highlight(item.something)" – Rich Finelli Dec 17 '14 at 14:36
0

If you are using the angular material library there is a built in directive called md-highlight-text

From the documentation:

<input placeholder="Enter a search term..." ng-model="searchTerm" type="text">
<ul>
  <li ng-repeat="result in results" md-highlight-text="searchTerm">
    {{result.text}}
  </li>
</ul>

Link to docs: https://material.angularjs.org/latest/api/directive/mdHighlightText

Anthony
  • 1,667
  • 2
  • 17
  • 28