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:
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
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 {}
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;
}
}
}
}
}
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 = "";
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;
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';
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;
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>`;
});
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
);
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>
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 ❤️
Top comments (4)
Hi, thanks for awesome tutorial! But I need help...
It works well but I have a problem that if I use this directive on element which is rendering text in ngFor, then my text isn't rendered at all, but the element is.
Example: (div highlight="a") (( item.name )) (/div)
(I used wrong brackets so I could post example)
Normally there would be a name of my item, but when I use this directive it only creates empty div. Help would be greatly appreciated! Thanks anyone in advance.
Okay I found a solution:
Instead of generating my items like: (div highlight="a") ((item.name)) (/div)
I just need to generate it like: (div highlight="a" [innerHtml]="item.name") (/div)
Not sure why, but it solves the problem.
nice solution, but what happens if the element has nested HTML tags? those are not returned by
textContent
right?Yup...if there is nested tags, we may need to either provide the text content as input or maybe pass a selector for the text element as input.