Angular Built-in Directives: *ngIf, *ngFor, ngSwitch, ngClass and Your Own Directive
Angular 12's built-in directives: *ngIf with else, *ngFor with trackBy, ngSwitch, ngClass, ngStyle, ng-container, the ng-template behind the asterisk, and your own attribute directive with HostListener.
Version note: The code and commands in this post are for Angular 12.
Directives: Labels That Add Behavior to Templates
In plain HTML, hiding an element based on a condition or repeating a list means writing JavaScript and changing the DOM by hand. In Angular you do these things inside the template, by adding an attribute to the element. Behind each of those attributes there is a directive.
A directive is a class that gives an element some behavior. In fact, the component we saw earlier is also a directive; it's just a directive with its own template. Apart from that there are two more kinds. Structural directives change the structure of the DOM: they add, remove and repeat elements. Attribute directives change the look or behavior of an element that already exists.
Think of the wiring in a house. A structural directive is like a circuit breaker; it decides whether a room gets power at all. An attribute directive is a dimmer; the lamp is already on, it just adjusts the light.
All the built-in directives in this post live in CommonModule. In AppModule we already get it through BrowserModule. We will use a small task list as the example:
export interface Task {
id: number;
title: string;
status: 'todo' | 'doing' | 'done';
priority: 'low' | 'high';
}
export class TaskListComponent {
isLoading = false;
tasks: Task[] = [
{ id: 1, title: 'Tasarımı gözden geçir', status: 'done', priority: 'low' },
{ id: 2, title: 'Login sayfasını yaz', status: 'doing', priority: 'high' },
{ id: 3, title: 'Testleri çalıştır', status: 'todo', priority: 'low' }
];
}
*ngIf: Show or Remove Based on a Condition
<p *ngIf="tasks.length === 0">Henüz görev yok.</p>
If the condition is true, the paragraph is added to the DOM; if it's false, it's never created. There's a subtle but important point here: *ngIf doesn't hide the element with CSS, it removes it from the DOM entirely. If the element is a component, that component is destroyed and created from scratch when the condition becomes true again.
If you want to show something else when the condition fails, you use else:
<div *ngIf="!isLoading; else loading">
{{ tasks.length }} görev var.
</div>
<ng-template #loading>
<p>Görevler yükleniyor...</p>
</ng-template>
The loading after else is the template reference variable we saw earlier. <ng-template> is a template that renders nothing on its own; its HTML is drawn only when something asks for it. When isLoading is true, Angular removes the div and puts this template in its place.
What's Behind the Asterisk?
The asterisk in *ngIf is shorthand. At compile time Angular turns the div above into this:
<ng-template [ngIf]="!isLoading" [ngIfElse]="loading">
<div>{{ tasks.length }} görev var.</div>
</ng-template>
So the element itself gets wrapped in an ng-template, and the ngIf directive is bound to that template with a regular property binding. The directive checks the condition and decides whether to stamp the template into the DOM. Every structural directive with an asterisk works the same way. Knowing this also makes the ng-container rule we will see shortly much easier to understand.
*ngFor: Rendering a List in One Line
<ul>
<li *ngFor="let task of tasks; let i = index">
{{ i + 1 }}. {{ task.title }}
</li>
</ul>
let task of tasks renders the li once for each item in the array and hands the current item to the template as task. It's like a cookie cutter: you prepare the cutter once, and you get as many cookies as you have dough. When the array changes, the list updates by itself.
index is one of the local variables ngFor provides. The others are at least as useful:
<li *ngFor="let task of tasks; let first = first; let last = last; let even = even"
[class.first]="first" [class.last]="last" [class.even]="even">
{{ task.title }}
</li>
first and last are true on the first and last items; perfect for styling the start or end of a list. even and odd separate even and odd rows, the classic way to build zebra-striped tables. You can also write these as index as i; both forms do the same thing.
Avoiding Needless Re-rendering with trackBy
Imagine you fetch the task list from the server again. Even if the content is the same, the objects that come back are brand-new objects. By default ngFor tracks items by object reference, so it decides "these are all new," removes every li and draws them again. With three tasks you won't notice; with three thousand, you will.
With trackBy you tell Angular how to recognize items:
trackById(index: number, task: Task): number {
return task.id;
}
<li *ngFor="let task of tasks; trackBy: trackById">{{ task.title }}</li>
Now Angular recognizes a task with the same id and leaves its DOM element where it is. It only touches rows that were really added, removed or moved. For lists that work with real data, trackBy is a habit worth building.
ngSwitch: More Than Two Cases
When a value has three or four possible states, nested *ngIfs get messy fast. To show the status of each task in the list, we write this instead:
<span [ngSwitch]="task.status">
<span *ngSwitchCase="'todo'">Bekliyor</span>
<span *ngSwitchCase="'doing'">Devam ediyor</span>
<span *ngSwitchCase="'done'">Tamamlandı</span>
<span *ngSwitchDefault>Bilinmiyor</span>
</span>
The structure mirrors switch in TypeScript. Two things to watch. [ngSwitch] is written without an asterisk because it is an attribute directive itself; it only says which value to look at. The real work is done by the starred *ngSwitchCases. And the single quotes around 'todo' are required; without them Angular looks for a property named todo.
ng-container: The Invisible Wrapper
Say you want to list only the unfinished tasks. The first idea is to put both asterisks on the same element:
<li *ngFor="let task of tasks" *ngIf="task.status !== 'done'">...</li>
Angular doesn't allow it:
error NG5002: Can't have multiple template bindings on one element. Use only one attribute prefixed with *
Remember the ng-template behind the asterisk: each asterisk wants to wrap the element in its own template, and one element can't sit inside two templates at once. The fix is to move the loop to an outer element. But if you add an extra div there, you end up with invalid HTML inside the ul and broken CSS. That's exactly what ng-container is for:
<ul>
<ng-container *ngFor="let task of tasks">
<li *ngIf="task.status !== 'done'">{{ task.title }}</li>
</ng-container>
</ul>
ng-container doesn't put any element into the DOM. Look at it in the browser's developer tools and you will see only the li elements inside the ul. Think of it as a moving box: it carries your things, but once you've moved in, no box is left in the room.
ngClass and ngStyle
The [class.x] binding we saw earlier is ideal for a single class. When you manage several classes based on conditions, ngClass keeps things tidier:
<li *ngFor="let task of tasks"
[ngClass]="{ 'done': task.status === 'done', 'urgent': task.priority === 'high' }"
[ngStyle]="{ 'opacity': task.status === 'done' ? 0.5 : 1 }">
{{ task.title }}
</li>
In the object you pass to ngClass, each key is a class name and each value is the condition that decides whether that class is applied. You can also pass a string or an array, but for conditional use the object form reads best. ngStyle sets style properties with the same logic.
A practical tip: instead of writing long style lists in ngStyle, define a CSS class and toggle it with ngClass. The styles stay in the CSS file and the decision stays in the template.
Our Own Attribute Directive
When the built-in directives aren't enough, you write your own. Let's generate a directive that highlights the row under the mouse:
ng g directive highlight
The CLI creates the directive file and its test file, and adds the directive to AppModule's declarations. Fill in the file like this:
import { Directive, ElementRef, HostListener } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
constructor(private el: ElementRef<HTMLElement>) { }
@HostListener('mouseenter') onMouseEnter(): void {
this.highlight('lightyellow');
}
@HostListener('mouseleave') onMouseLeave(): void {
this.highlight('');
}
private highlight(color: string): void {
this.el.nativeElement.style.backgroundColor = color;
}
}
The selector is in square brackets: [appHighlight]. It's the same idea as an attribute selector in CSS; the directive applies to every element that carries this attribute.
The ElementRef in the constructor gives access to the element the directive is applied to. We only have to list it as a constructor parameter and Angular hands it to us. That mechanism is called dependency injection, and we will cover it in upcoming posts.
@HostListener('mouseenter') listens to an event on the element the directive is attached to. When the mouse moves over the element, the background turns yellow; when it leaves, it goes back to normal. Using it takes a single word:
<li *ngFor="let task of tasks" appHighlight>{{ task.title }}</li>
A Small Experiment
Add a "Hide completed" button to the task list. Clicking it should flip a boolean, and with ng-container and *ngIf the completed tasks should disappear from the list. If no tasks are left, show an "All done" message with else.
Then use ngSwitch to put a label next to each task based on its status, and use ngClass to make high-priority tasks bold. Finally, apply the appHighlight directive to the list rows and check in the browser's developer tools that ng-container really leaves no trace in the DOM.
Our directive always uses the same yellow right now. To pass the color in from the outside, from where it's used, we need an input. That's exactly our next topic: passing data between components with inputs and outputs.