Highlight Search Results in AngularJS With a Filter

Highlight matching search terms in an AngularJS result list with a safe custom filter that escapes text and treats the query as a literal string.

Highlight Search Results in AngularJS With a Filter

A search result should show why it matched, not only that it survived the filter, so this AngularJS filter wraps a literal search term in a mark element after it escapes the source text.

What the filter needs to return

AngularJS can filter a repeated list and render a separate value through ng-bind-html, while the highlight filter returns markup where matching characters sit inside a mark element.

The text and query need different treatment.

Escape the source text because it can contain angle brackets, then escape the query for a regular expression because C++, brackets, and parentheses are ordinary search input rather than regex instructions.

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

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

The built-in filter decides which rows remain visible, and the custom highlight filter only changes the text inside each visible row.

Build a safe highlight filter

Place this code in the AngularJS module that owns the result list, where it escapes text first and treats the whole query as a literal string.

function escapeHtml(value) {
  return String(value)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

function escapeRegExp(value) {
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

angular.module('Demo', [])
  .filter('highlight', function($sce) {
    return function(text, phrase) {
      const safeText = escapeHtml(text);
      const query = String(phrase || '').trim();

      if (!query) {
        return $sce.trustAsHtml(safeText);
      }

      const expression = new RegExp(`(${escapeRegExp(query)})`, 'gi');
      const highlighted = safeText.replace(
        expression,
        '<mark class="highlighted">$1</mark>'
      );

      return $sce.trustAsHtml(highlighted);
    };
  });

The exact filter passed case-insensitive matches, C++ as a literal query, an empty query, a missing match, and source text containing an image tag in a Node.js test environment.

The source b tags remain escaped while C++ becomes the only marked text.

Terminal output showing escaped AngularJS highlight filter tests passing
The tested filter escapes source markup and highlights a literal C++ query.

Why escaping comes before trustAsHtml

ng-bind-html needs a trusted value to render the mark element instead of printing it as text.

Trusting the original item text without escaping it first allows any HTML in that text to reach the page, which is not appropriate for API responses, user content, or imported data.

Angular’s security guidance explains why HTML contexts need care, and this filter limits the trusted value to escaped source text plus the mark element.

Use Angular instead of AngularJS for a new application

This code is for maintaining an AngularJS application.

AngularJS support ended in January 2022, so a new application should use current Angular and its custom pipe model instead of adding a new AngularJS dependency.

Escape text, treat the query as literal input, and limit any HTML binding to markup your code created.

Connect the filter to a result list

Once the local list works, the same filter can sit beside results loaded from an API.

The Angular and Node.js live search example shows the server-backed list side, while the AngularJS tutorial gives the module and binding context behind this filter.

Keep the query in one model value and use it for both filtering and highlighting.

Test it with punctuation and text that came from the same source as the result list before you connect it to production data.

FAQ

These answers cover the limits that affect an AngularJS search highlight implementation.

Why does the AngularJS highlight filter escape the search query?

A search query can contain regular-expression characters such as plus signs, brackets, or parentheses. Escaping it makes the query match its literal characters instead of changing the regular expression.

Why use ng-bind-html for highlighted AngularJS text?

The filter returns a mark element around each match. ng-bind-html renders that element, but source text must be escaped before the result is passed to $sce.trustAsHtml.

Should a new application use this AngularJS filter?

No. AngularJS support ended in January 2022. Use current Angular for a new application and apply the same source-text and query-escaping boundaries in the implementation you choose.

Pankaj Kumar
Pankaj Kumar

Pankaj Kumar is the founder and CEO of CodeForGeek, with more than 14 years in IT. He is an open-source enthusiast who enjoys sharing what he learns through CodeForGeek and YouTube, with a focus on Python, data analytics, machine learning, Angular, Node.js, and Kafka.

Articles: 335