Using attribute directives to handle the appearance of elements
In this recipe, you'll work with an Angular attribute directive named highlight. With this directive, you'll be able to search words and phrases within a paragraph and highlight them on the go. The whole paragraph's container background will also be changed when we have a search in action.
Getting ready
The project we are going to work with resides in chapter02/start_here/ad-attribute-directive
, inside the cloned repository:
- Open the project in Visual Studio Code (VS Code).
- Open the terminal, and run
npm install
to install the dependencies of the project. - Once done, run
ng serve -o
.This should open the app in a new browser tab, and you should see something like this:
How to do it…
So far, the app has a search input box and a paragraph text. We need to be able to type a search query into the search box so that we can highlight the matching text in the paragraph. Here are the steps on how we achieve this:
- We'll create a property named
searchText
in theapp.component.ts
file that we'll use as a model for the search-text input:... export class AppComponent { Â Â title = 'ad-attribute-directive'; Â Â searchText = ''; }
- Then, we use this
searchText
property in theapp.component.html
file with the search input as angModel
, as follows:… <div class="content" role="main">   ...     <input [(ngModel)]="searchText" type="text"     class="form-control" placeholder="Search Text"     aria-label="Username" aria-describedby=    "basic-addon1">   </div>
Important note
Notice that
ngModel
doesn't work withoutFormsModule
, and so we've already importedFormsModule
into ourapp.module.ts
file. - Now, we'll create an attribute directive named
highlight
by using the following command inside ourad-attributes-directive
project:ng g d directives/highlight
- The preceding command generated a directive that has a selector called
appHighlight
. See the How it works… section for why that happens. Now that we have the directive in place, we'll create two inputs for the directive to be passed fromAppComponent
(fromapp.component.html
)—one for the search text and another for the highlight color. The code should look like this in thehighlight.directive.ts
file:import { Directive, Input } from '@angular/core'; @Directive({ Â Â selector: '[appHighlight]' }) export class HighlightDirective { Â Â @Input() highlightText = ''; Â Â @Input() highlightColor = 'yellow'; Â Â constructor() { } }
- Since we have the inputs in place now, let's use the
appHighlight
directive inapp.component.html
and pass thesearchText
model from there to theappHighlight
directive:<div class="content" role="main">   ...   <p class="text-content" appHighlight   [highlightText]="searchText">     ...   </p> </div>
- We'll listen to the input changes now for the
searchText
input, usingngOnChanges
. Please see the Using ngOnChanges to intercept input property changes recipe in Chapter 1, Winning Components Communication, for how to listen to input changes. For now, we'll only do aconsole.log
when the input changes:import { Directive, Input, SimpleChanges, OnChanges } from '@angular/core'; @Directive({ Â Â selector: '[appHighlight]' }) export class HighlightDirective implements OnChanges { Â Â ... Â Â ngOnChanges(changes: SimpleChanges) { Â Â Â Â if (changes.highlightText.firstChange) { Â Â Â Â Â Â return; Â Â Â Â } Â Â Â Â const { currentValue } = changes.highlightText; Â Â Â Â console.log(currentValue); Â Â } }
- Now, we'll write some logic for what to do when we actually have something to search for. For this, we'll first import the
ElementRef
service so that we can get access to the template element on which our directive is applied. Here's how we'll do this:import { Directive, Input, SimpleChanges, OnChanges, ElementRef } from '@angular/core'; @Directive({ Â Â selector: '[appHighlight]' }) export class HighlightDirective implements OnChanges { Â Â @Input() highlightText = ''; Â Â @Input() highlightColor = 'yellow'; Â Â constructor(private el: ElementRef) { } Â Â ... }
- Now, we'll replace every matching text in our
el
element with a custom<span>
tag with some hardcoded styles. Update yourngOnChanges
code inhighlight.directive.ts
as follows, and see the result:ngOnChanges(changes: SimpleChanges) {     if (changes.highlightText.firstChange) {       return;     }     const { currentValue } = changes.highlightText;     if (currentValue) {       const regExp = new RegExp(`(${currentValue})`,       'gi')       this.el.nativeElement.innerHTML =       this.el.nativeElement.innerHTML.replace       (regExp, `<span style="background-color:       ${this.highlightColor}">\$1</span>`)     } }
Tip
You'll notice that if you type a word, it will still just show only one letter highlighted. That's because whenever we replace the
innerHTML
property, we end up changing the original text. Let's fix that in the next step. - To keep the original text intact, let's create a property name of
originalHTML
and assign an initial value to it on the first change. We'll also use theoriginalHTML
property while replacing the values:... export class HighlightDirective implements OnChanges {   @Input() highlightText = '';   @Input() highlightColor = 'yellow';   originalHTML = '';   constructor(private el: ElementRef) { }   ngOnChanges(changes: SimpleChanges) {     if (changes.highlightText.firstChange) {       this.originalHTML = this.el.nativeElement.      innerHTML;       return;     }     const { currentValue } = changes.highlightText;     if (currentValue) {       const regExp = new RegExp(`(${currentValue})`,       'gi')       this.el.nativeElement.innerHTML =       this.originalHTML.replace(regExp, `<span       style="background-color: ${this.      highlightColor}">\$1</span>`)     }   } }
- Now, we'll write some logic to reset everything back to the
originalHTML
property when we remove our search query (when the search text is empty). In order to do so, let's add anelse
condition, as follows:... export class HighlightDirective implements OnChanges {   ...   ngOnChanges(changes: SimpleChanges) {    ...     if (currentValue) {       const regExp = new RegExp(`(${currentValue})`,       'gi')       this.el.nativeElement.innerHTML = this.      originalHTML.replace(regExp, `<span       style="background-color: ${this.      highlightColor}">\$1</span>`)     } else {       this.el.nativeElement.innerHTML =       this.originalHTML;     }   } }
How it works…
We create an attribute directive that takes the highlightText
and highlightColor
inputs and then listens to the input changes for the highlightText
input using the SimpleChanges
application programming interface (API) and the ngOnChanges
life cycle hook.
First, we make sure to save the original content of the target element by getting the attached element using the ElementRef
service, using the .nativeElement.innerHTML
on the element, and then saving it to originalHTML
property of the directive. Then, whenever the input changes, we replace the text with an additional HTML element (a <span>
element) and add the background color to this span
element. We then replace the innerHTML
property of the target element with this modified version of the content. That's all the magic!
See also
- Testing Angular attribute directives documentation (https://angular.io/guide/testing-attribute-directives)