Chapter 2: Understanding and Using Angular Directives
In this chapter, you'll learn about Angular directives in depth. You'll learn about attribute directives, with a really good real-world example of using a highlight directive. You'll also write your first structural directive and see how ViewContainer
and TemplateRef
services work together to add/remove elements from the Document Object Model (DOM), just as in the case of *ngIf
, and you'll create some really cool attribute directives that do different tasks. Finally, you'll learn how to use multiple structural directives on the same HyperText Markup Language (HTML) element and how to enhance template type checking for your custom directives.
Here are the recipes we're going to cover in this chapter:
- Using attribute directives to handle the appearance of elements
- Creating a directive to calculate the read time for articles
- Creating a basic directive that allows you to vertically scroll to an element
- Writing your first custom structural directive
- How to use
*ngIf
and*ngSwitch
together - Enhancing template type checking for your custom directives
Technical requirements
For the recipes in this chapter, make sure you have Git and Node.js installed on your machine. You also need to have the @angular/cli
package installed, which you can do with npm install -g @angular/cli
from your terminal. The code for this chapter can be found at https://github.com/PacktPublishing/Angular-Cookbook/tree/master/chapter02.
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:

Figure 2.1 – ad-attribute-directives app running on http://localhost:4200
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)
Creating a directive to calculate the read time for articles
In this recipe, you'll create an attribute directive to calculate the read time of an article, just like Medium. The code for this recipe is highly inspired by my existing repository on GitHub, which you can view at the following link: https://github.com/AhsanAyaz/ngx-read-time.
Getting ready
The project for this recipe resides in chapter02/start_here/ng-read-time-directive
:
- Open the project in 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:

Figure 2.2 – ng-read-time-directive app running on http://localhost:4200
How to do it…
Right now, we have a paragraph in our app.component.html
file for which we need to calculate the read time in minutes. Let's get started:
- First, we'll create an attribute directive named
read-time
. To do that, run the following command:ng g directive directives/read-time
- The preceding command created an
appReadTime
directive. We'll first apply this directive todiv
inside theapp.component.html
file with theid
property set tomainContent
, as follows:... <div class="content" role="main" id="mainContent" appReadTime> ... </div>
- Now, we'll create a configuration object for our
appReadTime
directive. This configuration will contain awordsPerMinute
value, on the basis of which we'll calculate the read time. Let's create an input inside theread-time.directive.ts
file with aReadTimeConfig
exported interface for the configuration, as follows:import { Directive, Input } from '@angular/core'; export interface ReadTimeConfig { wordsPerMinute: number; } @Directive({ selector: '[appReadTime]' }) export class ReadTimeDirective { @Input() configuration: ReadTimeConfig = { wordsPerMinute: 200 } constructor() { } }
- We can now move on to getting the text to calculate the read time. For this, we'll use the
ElementRef
service to retrieve thetextContent
property of the element. We'll extract thetextContent
property and assign it to a local variable namedtext
in thengOnInit
life cycle hook, as follows:import { Directive, Input, ElementRef, OnInit } from '@angular/core'; ... export class ReadTimeDirective implements OnInit { @Input() configuration: ReadTimeConfig = { wordsPerMinute: 200 } constructor(private el: ElementRef) { } ngOnInit() { const text = this.el.nativeElement.textContent; } }
- Now that we have our text variable filled up with the element's entire text content, we can calculate the time to read this text. For this, we'll create a method named
calculateReadTime
by passing thetext
property to it, as follows:... export class ReadTimeDirective implements OnInit { ... ngOnInit() { const text = this.el.nativeElement.textContent; const time = this.calculateReadTime(text); } calculateReadTime(text: string) { const wordsCount = text.split(/\s+/g).length; const minutes = wordsCount / this.configuration. wordsPerMinute; return Math.ceil(minutes); } }
- We've got the time now in minutes, but it's not in a user-readable format at the moment since it is just a number. We need to show it in a way that is understandable for the end user. To do so, we'll do some minor calculations and create an appropriate string to show on the user interface (UI). The code is shown here:
... @Directive({ selector: '[appReadTime]' }) export class ReadTimeDirective implements OnInit { ... ngOnInit() { const text = this.el.nativeElement.textContent; const time = this.calculateReadTime(text); const timeStr = this.createTimeString(time); console.log(timeStr); } ... createTimeString(timeInMinutes) { if (timeInMinutes === 1) { return '1 minute'; } else if (timeInMinutes < 1) { return '< 1 minute'; } else { return `${timeInMinutes} minutes`; } } }
Note that with the code so far, you should be able to see the minutes on the console when you refresh the application.
- Now, let's add an
@Output()
to the directive so that we can get the read time in the parent component and display it on the UI. Let's add it as follows in theread-time.directive.ts
file:import { Directive, Input, ElementRef, OnInit, Output, EventEmitter } from '@angular/core'; ... export class ReadTimeDirective implements OnInit { @Input() configuration: ReadTimeConfig = { wordsPerMinute: 200 } @Output() readTimeCalculated = new EventEmitter<string>(); constructor(private el: ElementRef) { } ... }
- Let's use the
readTimeCalculated
output to emit the value of thetimeStr
variable from thengOnInit()
method when we've calculated the read time:... export class ReadTimeDirective { ... ngOnInit() { const text = this.el.nativeElement.textContent; const time = this.calculateReadTime(text); const timeStr = this.createTimeString(time); this.readTimeCalculated.emit(timeStr); } ... }
- Since we emit the read-time value using the
readTimeCalculated
output, we have to listen to this output's event in theapp.component.html
file and assign it to a property of theAppComponent
class so that we can show this on the view. But before that, we'll create a local property in theapp.component.ts
file to store the output event's value, and we'll also create a method to be called upon when the output event is triggered. The code is shown here:... export class AppComponent { readTime: string; onReadTimeCalculated(readTimeStr: string) { this.readTime = readTimeStr; } }
- We can now listen to the output event in the
app.component.html
file, and we can then call theonReadTimeCalculated
method when thereadTimeCalculated
output event is triggered:... <div class="content" role="main" id="mainContent" appReadTime (readTimeCalculated)="onReadTimeCalculated($event)"> ... </div>
- Now, we can finally show the read time in the
app.component.html
file, as follows:<div class="content" role="main" id="mainContent" appReadTime (readTimeCalculated)="onReadTimeCalculated($event)"> <h4>Read time = {{readTime}}</h4> <p class="text-content"> Silent sir say desire fat him letter. Whatever settling goodness too and honoured she building answered her. ... </p> ... </div>
How it works…
The appReadTime
directive is at the heart of this recipe. We use the ElementRef
service inside the directive to get the native element that the directive is attached to, then we take out its text content. The only thing that remains then is to perform the calculation. We first split the entire text content into words by using the /\s+/g
regular expression (regex), and thus we count the total words in the text content. Then, we divide the word count by the wordsPerMinute
value we have in the configuration to calculate how many minutes it would take to read the entire text. Easy peasy, lemon squeezy.
See also
- Ngx Read Time library (https://github.com/AhsanAyaz/ngx-read-time)
- Angular attribute directives documentation (https://angular.io/guide/testing-attribute-directives)
Creating a basic directive that allows you to vertically scroll to an element
In this recipe, you'll create a directive to allow the user to scroll to a particular element on the page, on click.
Getting ready
The project for this recipe resides in chapter02/start_here/ng-scroll-to-directive
:
- Open the project in 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:

Figure 2.3 – ng-scroll-to-directive app running on http://localhost:4200
How to do it…
- First off, we'll create a
scroll-to
directive so that we can enhance our application with smooth scrolls to different sections. We'll do this using the following command in the project:ng g directive directives/scroll-to
- Now, we need to make the directive capable of accepting an
@Input()
that'll contain the Cascading Style Sheets (CSS) Query Selector for our target section that we'll scroll to upon the element'sclick
event. Let's add the input as follows to ourscroll-to.directive.ts
file:import { Directive, Input } from '@angular/core'; @Directive({ selector: '[appScrollTo]' }) export class ScrollToDirective { @Input() target = ''; constructor() { } }
- Now, we'll apply the
appScrollTo
directive to the links in theapp.component.html
file along with the respective targets so that we can implement the scroll logic in the next steps. The code should look like this:... <div class="content" role="main"> <div class="page-links"> <h4 class="page-links__heading"> Links </h4> <a class="page-links__link" appScrollTo target="#resources">Resources</a> <a class="page-links__link" appScrollTo target="#nextSteps">Next Steps</a> <a class="page-links__link" appScrollTo target="#moreContent">More Content</a> <a class="page-links__link" appScrollTo target="#furtherContent">Further Content</a> <a class="page-links__link" appScrollTo target="#moreToRead">More To Read</a> </div> ... <div class="to-top-button"> <a appScrollTo target="#toolbar" class= "material-icons"> keyboard_arrow_up </a> </div> </div>
- Now, we'll implement the
HostListener()
decorator to bind theclick
event to the element the directive is attached to. We'll just log thetarget
input when we click the links. Let's implement this, and then you can try clicking on the links to see the value of thetarget
input on the console:import { Directive, Input, HostListener } from '@angular/core'; @Directive({ selector: '[appScrollTo]' }) export class ScrollToDirective { @Input() target = ''; @HostListener('click') onClick() { console.log(this.target); } ... }
- Since we have the
click
handler set up already, we can now implement the logic to scroll to a particular target. For that, we'll use thedocument.querySelector
method, using thetarget
variable's value to get the element, and then theElement.scrollIntoView()
web API to scroll the target element. With this change, you should have the page being scrolled to the target element already when you click the corresponding link:... export class ScrollToDirective { @Input() target = ''; @HostListener('click') onClick() { const targetElement = document.querySelector (this.target); targetElement.scrollIntoView(); } ... }
- All right—we got the scroll working. "But what's new, Ahsan? Isn't this exactly what we were already doing with the href implementation before?" Well, you're right. But, we're going to make the scroll super smoooooth. We'll pass
scrollIntoViewOptions
as an argument to thescrollIntoView
method with the{behavior: "smooth"}
value to use an animation during the scroll. The code should look like this:... export class ScrollToDirective { @Input() target = ''; @HostListener('click') onClick() { const targetElement = document.querySelector (this.target); targetElement.scrollIntoView({behavior: 'smooth'}); } constructor() { } }
How it works…
The essence of this recipe is the web API that we're using within an Angular directive, and that is Element.scrollIntoView()
. We first attach our appScrollTo
directive to the elements that should trigger scrolling upon clicking them. We also specify which element to scroll to by using the target
input for each directive attached. Then, we implement the click
handler inside the directive with the scrollIntoView()
method to scroll to a particular target, and to use a smooth animation while scrolling, we pass the {behavior: 'smooth'}
object as an argument to the scrollIntoView()
method.
There's more…
scrollIntoView()
method documentation (https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView)- Angular attribute directives documentation (https://angular.io/guide/testing-attribute-directives)
Writing your first custom structural directive
In this recipe, you'll write your first custom structural directive named *appIfNot
that will do the opposite of what *ngIf
does—that is, you'll provide a Boolean value to the directive, and it will show the content attached to the directive when the value is false
, as opposed to how the *ngIf
directive shows the content when the value provided is true
.
Getting ready
The project for this recipe resides in chapter02/start_here/ng-if-not-directive
:
- Open the project in 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:

Figure 2.4 – ng-if-not-directive app running on http://localhost:4200
How to do it…
- First of all, we'll create a directive using the following command in the project root:
ng g directive directives/if-not
- Now, instead of the
*ngIf
directive in theapp.component.html
file, we can use our*appIfNot
directive. We'll also reverse the condition fromvisibility === VISIBILITY.Off
tovisibility === VISIBILITY.On
, as follows:... <div class="content" role="main"> ... <div class="page-section" id="resources" *appIfNot="visibility === VISIBILITY.On"> <!-- Resources --> <h2>Content to show when visibility is off</h2> </div> </div>
- Now that we have set the condition, we need to create an
@Input
inside the*appIfNot
directive that accepts a Boolean value. We'll use a setter to intercept the value changes and will log the value on the console for now:import { Directive, Input } from '@angular/core'; @Directive({ selector: '[appIfNot]' }) export class IfNotDirective { constructor() { } @Input() set appIfNot(value: boolean) { console.log(`appIfNot value is ${value}`); } }
- If you tap on the Visibility On and Visibility Off buttons now, you should see the values being changed and reflected on the console, as follows:
Figure 2.5 – Console logs displaying changes for the appIfNot directive values
- Now, we're moving toward the actual implementation of showing and hiding the content based on the value being
false
andtrue
respectively, and for that, we first need theTemplateRef
service and theViewContainerRef
service injected into the constructor ofif-not.directive.ts
. Let's add these, as follows:import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appIfNot]' }) export class IfNotDirective { constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) { } @Input() set appIfNot(value: boolean) { console.log(`appIfNot value is ${value}`); } }
- Finally, we can add the logic to add/remove the content from the DOM based on the
appIfNot
input's value, as follows:... export class IfNotDirective { constructor(private templateRef: TemplateRef<any>, private viewContainerRef: ViewContainerRef) { } @Input() set appIfNot(value: boolean) { if (value === false) { this.viewContainerRef. createEmbeddedView(this.templateRef); } else { this.viewContainerRef.clear() } } }
How it works…
Structural directives in Angular are special for multiple reasons. First, they allow you to manipulate DOM elements—that is, adding/removing/manipulating based on your needs. Moreover, they have this *
prefix that binds to all the magic Angular does behind the scenes. As an example, *ngIf
and *ngFor
are both structural directives that behind the scenes work with the <ng-template>
directive containing the content you bind the directive to and create the required variables/properties for you in the scope of ng-template
. In the recipe, we do the same. We use the TemplateRef
service to access the <ng-template>
directive that Angular creates for us behind the scenes, containing the host element on which our appIfNot
directive is applied. Then, based on the value provided to the directive as input, we decide whether to add the magical ng-template
to the view or clear the ViewContainerRef
service to remove anything inside it.
See also
- Angular structural directive microsyntax documentation (https://angular.io/guide/structural-directives#microsyntax)
- Angular structural directives documentation (https://angular.io/guide/structural-directives)
- Creating a structural directive by Rangle.io (https://angular-2-training-book.rangle.io/advanced-angular/directives/creating_a_structural_directive)
How to use *ngIf and *ngSwitch together
In certain situations, you might want to use more than one structural directive on the same host—for example, a combination of *ngIf
and *ngFor
together. In this recipe, you'll learn how to do exactly that.
Getting ready
The project we are going to work with resides in chapter02/start_here/multi-structural-directives
, inside the cloned repository:
- Open the project in 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:

Figure 2.6 – multi-structural-directives app running on http://localhost:4200
Now that we have the app running, let's see the steps for this recipe in the next section.
How to do it…
- We'll start by moving the element with the No items in bucket. Add some fruits! text into its own
<ng-template>
element, and we'll give it a template variable called#bucketEmptyMessage
. The code should look like this in theapp.component.html
file:… <div class="content" role="main"> ... <div class="page-section"> <h2>Bucket <i class="material-icons">shopping_cart </i></h2> <div class="fruits"> <div class="fruits__item" *ngFor="let item of bucket;"> <div class="fruits__item__title">{{item.name}} </div> <div class="fruits__item__delete-icon" (click)="deleteFromBucket(item)"> <div class="material-icons">delete</div> </div> </div> </div> </div> <ng-template #bucketEmptyMessage> <div class="fruits__no-items-msg"> No items in bucket. Add some fruits! </div> </ng-template> </div>
- Notice that we moved the entire
div
out of the.page-section
div. Now, we'll use thengIf-Else
syntax to either show a bucket list or an empty bucket message based on the bucket's length. Let's modify the code, as follows:... <div class="content" role="main"> ... <div class="page-section"> <h2>Bucket <i class="material-icons">shopping_cart </i></h2> <div class="fruits"> <div *ngIf="bucket.length > 0; else bucketEmptyMessage" class="fruits__item" *ngFor="let item of bucket;"> <div class="fruits__item__title">{{item.name}} </div> <div class="fruits__item__delete-icon" (click)="deleteFromBucket(item)"> <div class="material-icons">delete</div> </div> </div> </div> </div> ... </div>
As soon as you save the preceding code, you'll see the application breaks, mentioning we can't use multiple template bindings on one element. This means we can't use multiple structural directives on one element:
Figure 2.7 – Error on console, showing we can't use multiple directives on one element
- Now, as a final step, let's fix the issue by wrapping the div with
*ngFor="let item of bucket;"
inside an<ng-container>
element and using the*ngIf
directive on the<ng-container>
element, as follows:... <div class="content" role="main"> ... <div class="page-section"> <h2>Bucket <i class="material-icons">shopping_cart </i></h2> <div class="fruits"> <ng-container *ngIf="bucket.length > 0; else bucketEmptyMessage"> <div class="fruits__item" *ngFor="let item of bucket;"> <div class="fruits__item__title">{{item. name}}</div> <div class="fruits__item__delete-icon" (click)="deleteFromBucket(item)"> <div class="material-icons">delete</div> </div> </div> </ng-container> </div> </div> </div>
How it works…
Since we can't use two structural directives on a single element, we can always use another HTML element as a parent to use the other structural directive. However, that adds another element to the DOM and might cause problems for your element hierarchy, based on your implementation. <ng-container>
, however, is a magical element from Angular's core that is not added to the DOM. Instead, it just wraps the logic/condition that you apply to it, which makes it really easy for us to just add a *ngIf
or *ngSwitchCase
directive on top of your existing elements.
See also
- Group sibling elements with
<ng-container>
documentation (https://angular.io/guide/structural-directives#group-sibling-elements-with-ng-container)
Enhancing template type checking for your custom directives
In this recipe, you'll learn how to improve type checking in templates for your custom Angular directives using the static template guards that the recent versions of Angular have introduced. We'll enhance the template type checking for our appHighlight
directive so that it only accepts a narrowed set of inputs.
Getting ready
The project we are going to work with resides in chapter02/start_here/enhanced-template-type-checking
, inside the cloned repository:
- Open the project in 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:

Figure 2.8 – enhanced-template-type-checking app running on http://localhost:4200
Now that we have the app running, let's see the steps for this recipe in the next section.
How to do it…
- First off, we'll try to identify the problem, and that boils down to the ability to pass any string as a color to the
highlightColor
attribute/input for theappHighlight
directive. Give it a try. Provide the'#dcdcdc'
value as the input and you'll have a broken highlight color, but no errors whatsoever:... <div class="content" role="main"> ... <p class="text-content" appHighlight [highlightColor]="'#dcdcdc'" [highlightText]="searchText"> ... </p> </div>
- Well, how do we fix it? By adding some
angularCompileOptions
to ourtsconfig.json
file. We'll do this by adding a flag namedstrictInputTypes
astrue
. Stop the app server, modify the code as follows, and rerun theng serve
command to see the changes:{ "compileOnSave": false, "compilerOptions": { ... }, "angularCompilerOptions": { "strictInputTypes": true } }
You should see something like this:
Figure 2.9 – strictInputTypes helping with build time errors for incompatible type
- Well, great! Angular now identifies that the provided
'#dcdcdc'
value is not assignable to theHighlightColor
type. But what happens if someone tries to providenull
as the value? Would it still be fine? The answer is no. We would still have a broken experience, but no error whatsoever. To fix this, we'll enable two flags for ourangularCompilerOptions
—strictNullChecks
andstrictNullInputTypes
:{ "compileOnSave": false, "compilerOptions": { ... }, "angularCompilerOptions": { "strictInputTypes": true, "strictNullChecks": true, "strictNullInputTypes": true } }
- Update the
app.component.html
file to providenull
as the value for the[highlightColor]
attribute, as follows:... <div class="content" role="main"> ... <p class="text-content" appHighlight [highlightColor]="null" [highlightText]="searchText"> ... </div>
- Stop the server, save the file, and rerun
ng serve
, and you'll see that we now have another error, as shown here:Figure 2.10 – Error reporting with strictNullInputTypes and strictNullChecks in action
- Now, instead of so many flags for even further cases, we can actually just put two flags that do all the magic for us and cover most of our applications—the
strictNullChecks
flag and thestrictTemplates
flag:{ "compileOnSave": false, "compilerOptions": { ... }, "angularCompilerOptions": { "strictNullChecks": true, "strictTemplates": true } }
- Finally, we can import the
HighlightColor
enum into ourapp.component.ts
file. We will add ahColor
property to theAppComponent
class and will assign it a value from theHighlightColor
enum, as follows:import { Component } from '@angular/core'; import { HighlightColor } from './directives/highlight.directive'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { searchText = ''; hColor: HighlightColor = HighlightColor.LightCoral; }
- We'll now use the
hColor
property in theapp.component.html
file to pass it to theappHighlight
directive. This should fix all the issues and make light coral the assigned highlight color for our directive:<div class="content" role="main"> ... <p class="text-content" appHighlight [highlightColor]="hColor" [highlightText]="searchText"> ... </p> </div>
See also
- Angular structural directives documentation (https://angular.io/guide/structural-directives)
- Template type checking in Angular documentation (https://angular.io/guide/template-typecheck#template-type-checking)
- Troubleshooting template errors in Angular documentation (https://angular.io/guide/template-typecheck#troubleshooting-template-errors)