Demistify syntax of structural directive
Angular provides pretty detailed documentation about the structural directive. The document include syntax reference for it. However, it is still a bit confusing. I want to explain more about this in the following. There are two kind of tasks you can do in structural directive configuration,
@Input
binding- Template variable declaration.
@Input
binding
Let’s use built-in structural directive ngFor
and ngIf
for demo.
Here are the skeleton of ngIf
directive.
@Directive({
selector: "[ngIf]",
standalone: true,
})
export class NgIf<T = unknown> {
@Input()
set ngIf(condition: T) {}
@Input()
set ngIfThen(templateRef: TemplateRef<NgIfContext<T>> | null) {}
@Input()
set ngIfElse(templateRef: TemplateRef<NgIfContext<T>> | null) {}
}
You can see that all @Input
members start with prefix ngIf
, and
there is one @Input
named ngIf
, I call it default input
.
<div *ngIf="condition"></div>
For other input such as ngIfThen
, we use a convention
to convert a simplified input name. the convention is is to remove prefix, can
convert the remaining to be camel case. ngIfThen
is converted to then
and ngIfElse
is converted to else
.
The rule is that each @Input
property, use the simplified input name
follow by :
or space. Each input is separate by ;
So the following are
all correct.
<div *ngIf="condition; then: thenBlock; else: elseBlock"></div>
<div *ngIf="condition; then thenBlock; else elseBlock"></div>
<div *ngIf="condition; then thenBlock; else elseBlock"></div>
<div *ngIf="condition then thenBlock else elseBlock"></div>
The skeleton of ngFor
directive is as follow.
@Directive({
selector: "[ngFor][ngForOf]",
standalone: true,
})
export class NgForOf<T, U extends NgIterable<T> = NgIterable<T>>
implements DoCheck
{
@Input()
set ngForOf(ngForOf: (U & NgIterable<T>) | undefined | null) {
//..
}
@Input()
set ngForTrackBy(fn: TrackByFunction<T>) {
//..
}
set ngForTemplate(value: TemplateRef<NgForOfContext<T, U>>) {
//..
}
}
According to above rules, the following is correct. But actually it is partially correct. What is missing is template variable declaration.
<div *ngFor="of: ['one','two', 'three']; trackBy: trackById"></div>
<div *ngFor="of: ['one','two', 'three'] trackBy: trackById"></div>
<div *ngFor="of ['one','two', 'three'] trackBy trackById"></div>
Template variable declaration
When structral directive create a template, the template is linked to context object. The context object’s member can be used for data binding in template. But we still need manually to extract context’s member and assigned it to template variable.
All context has default member $implicit
. The following is interface
of the ngFor context.
export class NgForOfContext<T, U extends NgIterable<T> = NgIterable<T>> {
$implicit: T;
ngForOf: U;
index: number;
count: number;
first boolean;
last: boolean;
even: boolean;
odd: boolean;
}
Let’s say we want to create the following template variable.
let item = context.$implicit; //👉 let item; or let item = $implicit;
let i = context.index; // 👉 let i = index; or index as i;
let c = context.count; // 👉 let c = count; or count as c;
let isFirst = context.first;
// and so on
For ngFor
, it does not have a default @Input
( input with name ngFor
),
you always start with let
in the micro syntax.
So the binding markup can be one of the following.
<div
*ngFor="let item; let i = index; let c = count; of: ['one','two', 'three']; trackBy: trackById"
>
[{{i}}]: {{item}}, total {{c}}
</div>
<!-- or -->
<div
*ngFor="let item; let i = index; let c = count of: ['one','two', 'three']; trackBy: trackById"
>
[{{i}}]: {{item}}, total {{c}}
</div>
<!-- or -->
<div
*ngFor="let item of: ['one','two', 'three']; let i = index; let c = count trackBy: trackById"
>
[{{i}}]: {{item}}, total {{c}}
</div>
<!-- or -->
<div
*ngFor="let item of: ['one','two', 'three']; index as i; count as c; trackBy: trackById"
>
[{{i}}]: {{item}}, total {{c}}
</div>
For ngIf
, it does have a default @Input
( input with name ngIf
),
you always start with default @Input
binding, without using let
.
The template variable declaration will follow after that.
<div *ngIf="'hello';let value = $implicit">{{value}}</div>
<div *ngIf="'hello';let value">{{value}}</div>
<div *ngIf="'hello';$implicit as value">{{value}}</div>
<!-- output is
<div>
hello
</div>
-->
Because ngIf
also has additional member ngIf
like the following,
which has the same value of $implicit. The following binding is also legal.
export class NgIfContext<T = unknown> {
$implicit: T = null!;
ngIf: T = null!;
}
<div *ngIf="'hello';let value = ngIf">{{value}}</div>
<div *ngIf="'hello';ngIf as value">{{value}}</div>
The question is why we want to have this contxt.ngIf
. The answer is
we want to support the following syntax.
<div *ngIf="'hello' as value">{{value}}</div>
The above is same as
<div *ngIf="'hello';ngIf as value">{{value}}</div>
This syntax is useful in some cases. For example, if we have a observable,
and we want to use it in muliple times. So the following code is a way to do
it. But the problems is that the async
pipeline subscribe the observable
multiple times, which can cause multiple network fetch. One way to do
it, subscribe it manually programatically, and convert it to json object.
But that is not a nice solution.
<h2>hello, {{(user$ | async).first}} {{(user$ | async).last}}</h2>
If we want do it declaratively in template, we can use this syntax to convert a observable to json object and store it to a template variable first, then use the variable in multiple places like below.
<h2 *ngIf="(user$ | async) as user">hello, {{user.first}} {{user.last}}</h2>