Highlight text in paragraphs with a simple directive in Angular

Adithya Sreyaj - Jun 1 '21 - - Dev Community

How to highlight text in a paragraph with the help of directives in Angular. Especially helpful in highlighting text matching the search term. You could have come across this in your browser or IDE when you search for something, the matching items will be highlighted to point you to the exact place of occurrence.

Text Highlighting

Here is what we are going to build in this post. A very simple and straightforward highlight directive in Angular. We see something similar in chrome dev tools.

The idea is pretty simple. We just have to match the searched term and somehow wrap the matched text in a span or mark (ref) tag so that we can style them later according to our needs.

How to highlight matched text?

We are going to use Regex to find matches in our paragraph. Regex makes it very simple to do operations like this on strings. The directive should be ideally added only to elements with text in it.
This is what we are building:

Highlight text directive

So let's plan out our directive.
The main input to the directive is the term that needs to be highlighted. So yeah, we will use @Input() to pass the term to our directive. I think that is pretty much what we need inside the directive.

So now we need to get hold of the actual paragraph to search in. So there is an easy way to get the text from an HTMLElement. We can use the textContent(ref) which should give us the text to search in.

Building the Highlight directive

As always, I would recommend you create a new module only for the directive. And If you really properly manage your code base, you can consider creating it as a library within the project as well.

To keep things simple, we put our code in a lib folder:

lib/
├── highlight/
│   ├── highlight.module.ts
│   ├── highlight.directive.ts
Enter fullscreen mode Exit fullscreen mode

Highlight Module

This module would be simply declaring our directive and exporting it. Nothing much is needed here.

import { CommonModule } from "@angular/common";
import { NgModule } from "@angular/core";
import { HighlightDirective } from "./highligh.directive";

@NgModule({
  declarations: [HighlightDirective],
  imports: [CommonModule],
  exports: [HighlightDirective]
})
export class HighlightModule {}
Enter fullscreen mode Exit fullscreen mode

Highlight Directive

Now that our setup is complete, we can start creating our directive where all our magic is going to happen.

import {
  Directive,
  ElementRef,
  HostBinding,
  Input,
  OnChanges,
  SecurityContext,
  SimpleChanges
} from "@angular/core";
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";

@Directive({
  selector: "[highlight]"
})
export class HighlightDirective implements OnChanges {
  @Input("highlight") searchTerm: string;
  @Input() caseSensitive = false;
  @Input() customClasses = "";

  @HostBinding("innerHtml")
  content: string;
  constructor(private el: ElementRef, private sanitizer: DomSanitizer) {}

  ngOnChanges(changes: SimpleChanges) {
    if (this.el?.nativeElement) {
      if ("searchTerm" in changes || "caseSensitive" in changes) {
        const text = (this.el.nativeElement as HTMLElement).textContent;
        if (this.searchTerm === "") {
          this.content = text;
        } else {
          const regex = new RegExp(
            this.searchTerm,
            this.caseSensitive ? "g" : "gi"
          );
          const newText = text.replace(regex, (match: string) => {
            return `<mark class="highlight ${this.customClasses}">${match}</mark>`;
          });
          const sanitzed = this.sanitizer.sanitize(
            SecurityContext.HTML,
            newText
          );
          this.content = sanitzed;
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's do a code breakdown.

The first thing that we need is the Inputs in our directive. We only actually need the search term, but I have added some extra functionalities to our directive. We have an option to provide customClasses for the highlighted text, and another flag caseSensitive which will decide whether we have to match the case or not.

@Input("highlight") searchTerm: string;
@Input() caseSensitive = false;
@Input() customClasses = "";
Enter fullscreen mode Exit fullscreen mode

Next up we add a HostBinding (ref) which can be used to add value to a property on the host element.

@HostBinding("innerHtml")
 content: string;
Enter fullscreen mode Exit fullscreen mode

We bind to the innerHtml (ref) property of the host element. We can also do it in this way:

this.el.nativeElement.innerHtml = 'some text';
Enter fullscreen mode Exit fullscreen mode

To get access to the host element, we inject ElementRef in the constructor, and also since we are going to be playing around with direct HTML manipulation, I have also injected DomSanitizer (ref) to sanitize the HTML before we inject it into the element.

So now we move on to the actual logic which we can write in the ngOnChanges (ref) lifecycle hook. So when our search term changes, we can update the highlights. The interesting part is:

const regex = new RegExp(this.searchTerm,this.caseSensitive ? "g" : "gi");
const newText = text.replace(regex, (match: string) => {
     return `<mark class="highlight ${this.customClasses}">${match}</mark>`;
});
const sanitzed = this.sanitizer.sanitize(
    SecurityContext.HTML,
    newText
);
this.content = sanitzed;
Enter fullscreen mode Exit fullscreen mode

First, we set up the regex to help us find the matches. based on the caseSensitive condition we just add different Regex Flags:

  • g - search for all matches.
  • gi -search for all matches while ignoring case.

We just wrap the matches with mark tag using the replace (ref) method on the string.

const newText = text.replace(regex, (match: string) => {
     return `<mark class="highlight ${this.customClasses}">${match}</mark>`;
});
Enter fullscreen mode Exit fullscreen mode

After that the newText, which is a HTML string needs to be sanitized before we can bind it to the innerHTML. We use the sanitize (ref) method on the DomSanitizer class:

const sanitzed = this.sanitizer.sanitize(
    SecurityContext.HTML,
    newText
);
Enter fullscreen mode Exit fullscreen mode

Now we just assign the sanitized value to our content property which gets added to the innerHTML via HostBinding.

Usage

This is how we can use it in our component. Make sure to import our HighlightModule to make our directive available for use in the component.

<p [highlight]="searchTerm" [caseSensitive]="true" customClasses="my-highlight-class">
      Lorem Ipsum has been the industry's standard dummy text ever since the
      1500s, when an unknown printer took a galley of type and scrambled it to
      make a type specimen book.
</p>
Enter fullscreen mode Exit fullscreen mode

That's all! We've successfully created a very simple text highlighter in Angular using directives. As always, please don't directly reuse the code above, try to optimize it and you can always add or remove features to it.

Demo and Code

CodeSandbox: https://codesandbox.io/s/ng-highlight-11hii

Connect with me

Do add your thoughts in the comments section.
Stay Safe ❤️

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .