In the last article, we talked about attribute directives. In a natural continuation of the theme, we’ll now mention structural directives, too. So what are they, how do they differ from attribute directives, and what are we using them for?
Structural directives
In short, structural directives change the layout of the DOM by appending or removing DOM elements. The most commonly used structural directive is *ngIf
which conditionally shows or removes views from the template. There is also *ngFor
which renders lists of items, and *ngSwitch
, used for switching between alternate views.
<div *ngIf="clicks === 5">Only show if the variable clicks is equal to 5!</div> <tr *ngFor="let invoice of invoices"> <td>{{ invoice.id }}</td> </tr> <ul [ngSwitch]="fruit"> <li *ngSwitchCase="'Apples'">Apples</li> <li *ngSwitchCase="'Bananas'">Bananas</li> <li *ngSwitchCase="'Cherries'">Cherries</li> <li *ngSwitchDefault>Oranges</li> </ul>
Above we created an *ngIf
check which will render the context of the div
element only if the variable clicks
is equal to 5. There is another interesting, but not well-known *ngIf
functionality. We can (optionally) provide the alternative template by writing else
.
<div *ngIf="clicks === 5; else elseBlock">Only show if the variable clicks is equal to 5!</div> <ng-template #elseBlock>Show otherwise.</ng-template>
The *ngFor
or *ngForOf
directive renders a template for every element in an iterable collection. We can additionally use exported values in an *ngFor
directive such as index
, first
and count
. Additionally, we can provide a custom trackBy
method to optimize the rendering of the directive.
<li *ngFor=" let invoice of invoices; index as i; count as count; trackBy: trackByFn " > {{ invoice.id }} <span>Total: {{ count }} elements</span> </li>
The *ngSwitch
the directive will specify a condition to match, that will render all the elements that match that condition. In the example we matched the switch expression to a variable named fruit
in our component. Depending on the value of the variable we would match the condition (or multiple) that are truth. Otherwise, we would print out the default value:
<ul [ngSwitch]="fruit"> <li *ngSwitchCase="'Apples'">Apples</li> <li *ngSwitchCase="'Bananas'">Bananas</li> <li *ngSwitchCase="'Cherries'">Cherries</li> <li *ngSwitchDefault>Oranges</li> </ul>
Custom directive
There is a pretty cool specialized statement in Ruby (and Perl and some other languages), known as unless
, which does the exact opposite of what if
does, namely, it executes the statement only if the statement is false. So let us do something similar in Angular. We’ll use a slightly modified project from the article on attribute directives.
First, we’ll generate the unless directive.
import { Directive } from '@angular/core'; @Directive({ selector: '[appUnless]' }) export class UnlessDirective { constructor() { } }
Next thing, let us create a component variable called shouldHide
, and assign it to the template as well.
<div class="wrapper"> <div class="button-wrappers"> <button (click)="show()">Show</button> <button (click)="hide()">Hide</button> </div> <p *appUnless="shouldHide"> ... </p> </div>
export class TextCardComponent implements OnInit { shouldHide: boolean = false; constructor() {} ngOnInit() {} show(): void { this.shouldHide = false; } hide(): void { this.shouldHide = true; } }
The next thing to do is set up the directive itself. In order to track all the changes to the input variable of shouldHide
, we’ve created a setter (another alternative would have been to create the onChanges
lifecycle hook).
import { Directive, Input } from '@angular/core'; @Directive({ selector: '[appUnless]', }) export class UnlessDirective { @Input() set appUnless(shouldHide: boolean) { console.log(shouldHide); } constructor() {} }
Next, we’ll write the implementation. We’ll use the TemplateRef
and ViewContainerRef
classes, in order to instantiate embedded views based on a template. For more information, you can refer to this post on this blog.
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; @Directive({ selector: '[appUnless]', }) export class UnlessDirective { constructor( private templateRef: TemplateRef<HTMLElement>, private vcRef: ViewContainerRef ) {} @Input() set appUnless(shouldHide: boolean) { if (shouldHide) { this.vcRef.clear(); } else { this.vcRef.createEmbeddedView(this.templateRef); } } }
Now this works exactly like an *ngIf
would. The difference is that we inversed the logic in the directive itself, so we’re actually triggering the embedded view creating if the variable provided is false
.
The StackBlitz for this project can be found below: