Angular Custom Directives – Part 2: Structural Directives
210404
Note that this the 2nd part of a 2-part post. 1st part covers some intro on Angular Directives and then it presents a step-by-step example with code, for a Custom Attribute Directive. Here we continue with a Custom Structural Directive
You can find and download the code at https://github.com/zzpzaf/directivetest1/tree/8089a04b870fcef2d0f148dc7346d20976ae7c90
Short Intro to Angular Directives
The concept of the ‘Directive’ comes from the early versions of Angular (e.g., AngularJS 1.X) The concept covers almost all the basic building blocks of Angular, including Components. (We should consider Components as special type of Directives).
According Angular docs, there are three types of directives:
- Components
- Structural directives
- Attribute directives
Actually, directives provide us with the ground to attach and deal with the behavior of the DOM and its elements. To understand the pragmatic difference between Angular directives, we can keep in mind the following simple rule:
- If it has a template, it is a Component.
- else if it has a selector in brackets “[my-directive1]”, it is for sure, a Directive. If it is applied to an element like that: <div my-directive1> … </div> then this is an Attribute Directive.
- else it is applied to an element with an asterisk * like that: <div *my-directive1> … </div> ) then this is a Structural Directive.
So apart, from Components, Directives can’t have their own UI (template), but they can be attached to a component or a regular (‘native’) HTML element to change their visual representation.
You can read more at official documentation at https://angular.io/guide/built-in-directives
STRUCTURAL DIRECTIVES
Structural directives are mainly used to change the structure of a UI DOM element. When a Structural Directive is attached/applied to a template element, it provides possibilities to structure/de-structure the DOM and its elements. Using this type of directive, we can change the structure of any DOM element and can redesign or redecorate those DOM elements.
Angular provides us with some built-in structure directives. These are:
- NgIf—conditionally creates or disposes of subviews from the template.
- NgFor—repeat a node for each item in a list.
- NgSwitch—a set of directives that switch among alternative views
<div my-direct >...</div>
The most common Angular built-in Attribute Directives are:
- NgClass – adds and removes a set of CSS classes.
- NgStyle – adds and removes a set of HTML styles.
- NgModel – adds two-way data binding to an HTML form element.
Read more at: https://angular.io/guide/built-in-directives#built-in-structural-directives
Let’s now continue the project we have started in 1st part and add a custom Structural Directive.
Create and use a custom Structural Directive
Create a new Component
First, we are going to create a new component. This is the component that a new directive (we will create at the next step), will be targeted for changing its behavior (actually, the color of one of its elements). We will use the VS Code Terminal to create the my-component2 component:
ng g component my-component2
➜ directivetest1 git:(master) ✗ ng g component my-component2 CREATE src/app/my-component2/my-component2.component.css (0 bytes) CREATE src/app/my-component2/my-component2.component.html (28 bytes) CREATE src/app/my-component2/my-component2.component.spec.ts (669 bytes) CREATE src/app/my-component2/my-component2.component.ts (302 bytes) UPDATE src/app/app.module.ts (722 bytes) ➜ directivetest1 git:(master) ✗
Change a bit its template:
<hr> <div> <p>This is a component having a Class named 'MyComponent2Component' with selector 'app-my-component2'.</p> </div> <hr>
Add it as 2nd component in our app’s root element’s template, which of course is the template of our previous 1st component:
<hr> <div> <h2>Angular Demo</h2> <h3> Today is : {{curDate | date : "dd.MM.y"}} The Time is: <span appMyColorChanger1 myDateTime = {{curDate}} > {{curDate | date : "H.mm.ss"}} </span> </h3> <p>This is a component having a Class named 'MyComponent1Component' with selector 'app-my-component1'.</p> </div> <hr> <app-my-component2> </app-my-component2>
And check the result:
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-my-component1', templateUrl: './my-component1.component.html', styleUrls: ['./my-component1.component.css'] }) export class MyComponent1Component implements OnInit { curDate: Date = new Date(); constructor() { } ngOnInit(): void { } }
And to its template, e.g.:
Now, let’s use -directly into the template- of our new 2nd component, the Angular built-in NgFor structure directive to display an array of elements:
<hr> <div> <p>This is the 2nd component having a Class named 'MyComponent2Component' with selector 'app-my-component2'.</p> <hr> <h2>Numbers : </h2> <hr> <p *ngFor="let n of [112, 3, 9, 6, 19, 5, 4, 17]"> {{n}}</p> </div> <hr>
And again, check the result:
Of, course the same result can be achieved using a property/variable in the component class, like that:
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-my-component2', templateUrl: './my-component2.component.html', styleUrls: ['./my-component2.component.css'] }) export class MyComponent2Component implements OnInit { myArray: number[] = []; constructor() { } ngOnInit(): void { //console.log("======= 2nd Component ========="); this.myArray = [222, 3, 9, 6, 19, 5, 4, 17]; } }
And chang its template accordingly:
<hr> <div> <p>This is the 2nd component having a Class named 'MyComponent2Component' with selector 'app-my-component2'.</p> <hr> <h2>Numbers : </h2> <hr> <p *ngFor="let n of myArray"> {{n}}</p> </div> <hr>
The result is pretty similar:
Before proceed in creating a new custom Structural Directive, let’s look first on how we can show or not the Numbers section of our template, on the basis of an empty or not array.
We can do that by just using the built-in NgIf directive and check the condition in the template, like that:
<hr> <div> <p>This is the 2nd component having a Class named 'MyComponent2Component' with selector 'app-my-component2'.</p> <hr> <!-- Note that ngIf requires an Uppercase I, not i --> <div *ngIf = "myArray.length > 0"> <h2>Numbers : </h2> <hr> <p *ngFor="let n of myArray"> {{n}}</p> </div> </div> <hr>
You can very easily check it by your own, by setting a different number than 0 into the *ngIf condition, (e.g. for the table we have previously used this can be 8: <div *ngIf = “myArray.length > 8″>).
However, as we’ve said, our intention is to create our own custom Structural Directive. So it’s time to do it:
Create a new Structural Directive
We will use the VS Code Terminal to create the my-arrayCheck1 directive:
ng g d my-arrayCheck1
➜ directivetest1 git:(master) ✗ ng g d my-arrayCheck1 CREATE src/app/my-array-check1.directive.spec.ts (254 bytes) CREATE src/app/my-array-check1.directive.ts (155 bytes) UPDATE src/app/app.module.ts (820 bytes) ➜ directivetest1 git:(master) ✗
The auto-generated code is almost identical to the one we have previously seen with the Attribute Directive:
import { Directive } from '@angular/core'; @Directive({ selector: '[appMyArrayCheck1]' }) export class MyArrayCheck1Directive { constructor() { } }
In most of the cases, a Structural Directive, creates and deals with an embedded view based on built-in <ng-template>
. To deal with that embedded view we need to put it in a view container. To achieve that, 2 abstract classes should be injected inside the constructor method of our directive. These are:
- TemplateRef
- ViewContainerRef
We can access a TemplateRef instance by placing a directive on an <ng-template> element, or by using a structural directive (prefixed with *). So, TemplateRef helps us get to the <ng-template> contents. TemplateRef is actually a class (an abstract class). Using it (e.g. injecting it into a component’s/directive’s constructor) we can reference the <ng-template> inside that component or inside that directive (which in our case, this is our Structural Directive). Using the TemplateRef we can even manipulate the template content using our code.
ViewContainerRef, holds a reference of the host element. The host element is the element to which our directive has been applied with asterisk (We say that it hosts our directive).
Read more at: https://angular.io/api/core/TemplateRef
and: https://angular.io/api/core/ViewContainerRef#element
So, injecting both of them, the TemplateRef and the ViewContainerRef in our directive, we can accesses the view container of the host element our Directive is attached to. Simply, we can do it, like that:
. . . export class MyArrayCheck1Directive { constructor(myTemplateRef: TemplateRef<any>, myViewContainer: ViewContainerRef) { } }
Next step is to use an @Input decorator to pass-in to the directive our array, as parameter from the element that it will be applied. The @Input this time should be combined with a setter set method in, like that:
. . . @Input() set appMyArrayCheck1(theArray: number[]) { } . . .
That done, we are ready now to apply the directive to our component. Actually, we are going to replace the *ngIf built-in structural directive, with ours, like that:
<div *appMyArrayCheck1 = "myArray">
With *appMyArrayCheck1 we apply the directive to the DOM element (here this is the: <div. . .></div> element) and with = “myArray” we pass in it the array to be checked.
The component’s template becomes:
<hr> <div> <p>This is the 2nd component having a Class named 'MyComponent2Component' with selector 'app-my-component2'.</p> <hr> <!-- Note that ngIf requires an Uppercase I, not i --> <!-- <div *ngIf = "myArray.length > 0"> --> <div *appMyArrayCheck1 = "myArray"> <h2>Numbers : </h2> <hr> <p *ngFor="let n of myArray"> {{n}}</p> </div> </div> <hr>
Now let’s go back to the directive. What we need first, is to have a method/function to check if an array of numbers is sorted or not. We can use an external function or a directive’s method. Here we will implement a very simple recursive method for array sort check. The method will stop and will return 0 when it finds the 1 occurrence of an unsorted pair of neighbor numbers. Otherwise, it continues checking and finally -when it reaches to the last element and all checks were OK it returns 1. This is the whole method in the directive:
. . . // This is a recursive function that returns 0 if a pair is found unsorted arraySortCheck(arr: number[], n: number): number { // Array has one or no element or the // rest are already checked and approved. if (n == 1 || n == 0) return 1; // Unsorted pair found (Equal values allowed) if (arr[n - 1] < arr[n - 2]) return 0; // Last pair was sorted // Keep on checking return this.arraySortCheck(arr, n - 1); } . . .
After that we can call it from the @Input setter method and see if it does not return 0 or not, e.g.
. . . @Input() set appMyArrayCheck1(theArray: number[]) { let n: number = theArray.length; let res: number = this.arraySortCheck(theArray, n); if (res != 0) { this.myViewContainer.createEmbeddedView(this.myTemplateRef) } else { this.myViewContainer.clear(); } } . . .
If the array is sorted (the arraySortCheck returns 1) then creates the template view, which -in our case- it does mean that the whole content inside the <div. . .></div> element is displayed (i.e. our array). Otherwise, it simply clears it.
You can check it. If you leave the array we have defined in our component,
this.myArray = [222, 3, 9, 6, 19, 5, 4, 17];
then you can not see it. Otherwise if uou use a sorted array like
this.myArray = [3, 4, 5, 6, 9, 17, 19, 222];
then it works:
As a final step, we are going to use a button in our component. The button will serve to sort/unsort the array. So, according to that and since our directive checks, if the array is sorted or not, the result will be to show the (sorted) array or not to display it at all!
This is the component’s template with a button:
<hr> <div> <p>This is the 2nd component having a Class named 'MyComponent2Component' with selector 'app-my-component2'.</p> <hr> <!-- Note that ngIf requires an Uppercase I, not i --> <!-- <div *ngIf = "myArray.length > 0"> --> <div> <button (click)="ToggleSortMyArray()"> Array Sorrted: {{sorted ? 'True' : 'False'}} </button> <div *appMyArrayCheck1 = "myArray"> <h2> Numbers : </h2> <hr> <p *ngFor="let n of myArray"> {{n}}</p> </div> </div> </div> <hr>
And this is our component class with the toggle function (fired each time we click on the button):
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-my-component2', templateUrl: './my-component2.component.html', styleUrls: ['./my-component2.component.css'] }) export class MyComponent2Component implements OnInit { myArray: number[] = []; sorted: boolean = false; constructor() { } ngOnInit(): void { //console.log("======= 2nd Component ========="); this.myArray = this.getInitArray(); } getInitArray(): number[] { return [222, 3, 9, 6, 19, 5, 4, 17]; } sortTheArray(arr: number[]): number[]{ return arr.sort((n1,n2) => n1 - n2); } ToggleSortMyArray() :void{ if (this.sorted == false) { this.myArray = this.sortTheArray(this.getInitArray()); } else { this.myArray = this.getInitArray(); } this.sorted = ! this.sorted; //console.log(this.myArray); } }
And this is our directive after we made some improvements, such as to check if the has been created or not, so we can avoid adding again and again the view each time the condition is true, i.e. each time the array is found sorted.
import { Directive, ElementRef, Input, TemplateRef, ViewChild, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appMyArrayCheck1]' }) export class MyArrayCheck1Directive { private viewCreated: boolean = false; private elRef!: ElementRef; constructor(private myTemplateRef: TemplateRef<any>, private myViewContainer: ViewContainerRef) { } @Input() set appMyArrayCheck1(theArray: number[]) { let n: number = theArray.length; let res: number = this.arraySortCheck(theArray, n); if ( res != 0 && !this.viewCreated) { this.myViewContainer.createEmbeddedView(this.myTemplateRef) this.viewCreated = true; } else if (res == 0 && this.viewCreated) { this.myViewContainer.clear(); this.viewCreated = false; } } // This is a recursive function that returns 0 if a pair is found unsorted arraySortCheck(arr: number[], n: number): number { // Array has one or no element or the // rest are already checked and approved. if (n == 1 || n == 0) return 1; // Unsorted pair found (Equal values allowed) if (arr[n - 1] < arr[n - 2]) return 0; // Last pair was sorted // Keep on checking return this.arraySortCheck(arr, n - 1); } }
And this is the final result:
You can download the code from GitHub at https://github.com/zzpzaf/directivetest1/archive/b0519eaf94918f3f937aea6d01a5ef4b6964dc09.zip
That’s it for now!
Thanx for reading!