May 15th, 2020 - written by Kimserey with .
Structural directives in Angular, like ngIf
and ngFor
are mysterious directives. They both come with custom notation in order to define the content of the directives for example *ngIf="abc else xyz"
and *ngFor="let x of xs; let i=index; trackBy: trackBy
. In today’s post we will demystify the notation of NgIf
, called a microsyntax, by looking at how it can be used, and then reconstructing the directive ourselves.
NgIf
UsageStructural directive in contrast to regular directive modify the DOM by adding or removing part of it. In this post we will be look at NgIf
, the structural directive that adds or removes an element depending on a predicate expression provided. For example we can use NgIf
as followed:
1
2
3
<div *ngIf="show">
Hello World
</div>
which will show Hello World
if show
resolves to true
. NgIf
also allows us to specify an else
condition:
1
2
3
4
5
6
<div *ngIf="show; else noshow">
Hello World
</div>
<ng-template #noshow>
No show
</ng-template>
which will show the noshow
template referenced by the template reference variable #noshow
. We can also name the expression result and use it in the template by using as
:
1
2
3
4
5
6
<div *ngIf="(show$ | async) as show; else noshow">
Hello World {{show}}
</div>
<ng-template #noshow>
No show
</ng-template>
show
can then be used as a variable within the template. NgIf
also allows us to define the true
condition display elsewhere just like we did for #noshow
using then
:
1
2
3
4
5
6
7
<div *ngIf="show$ | async; then show; else noshow"></div>
<ng-template #show let-show>
Hello World {{show}}
<ng-template>
<ng-template #noshow>
No show
</ng-template>
which will display the show
template if shouldShow
is true
, else noshow
. In order to make show
accessible in the template, we define it with let-show
. We will see later the usage of let-*
in more details.
NgIf
Desugared SyntaxSo far we seen that NgIf
directive is used with the asterisk (*) which is the way to use structural directives. And we’ve seen that the string provided to the directive is constructed using the expression resulting in a boolean, plus few keywords; then
and else
.
The asterisk is actually a syntactic sugar which provides a quick way of using a structural directive. If we take back the previous examples:
1
2
3
<div *ngIf="show">
Hello World
</div>
would be desugared as:
1
2
3
4
5
<ng-template [ngIf]="show">
<div>
Hello world
</div>
</ng-template>
We have a ng-template
containing the div
and the directive ngIf
is assigned the expression show
.
1
2
3
4
5
6
<div *ngIf="show; else noshow">
Hello World
</div>
<ng-template #noshow>
No show
</ng-template>
would be desugared as:
1
2
3
4
5
6
7
8
<ng-template [ngIf]="show" [ngIfElse]="noshow">
<div>
Hello world
</div>
</ng-template>
<ng-template #noshow>
No show
</ng-template>
As we can see, ngIfElse
is an input from NgIf
which is then translated into a key usable in the syntax else noshow
where noshow
is the template reference. We will see in the implementation how the template reference is provided in the ngIfElse
input.
Lastly the following:
1
2
3
4
5
6
<div *ngIf="(show$ | async) as show; else noshow">
Hello World {{show}}
</div>
<ng-template #noshow>
No show
</ng-template>
would be desugared as:
1
2
3
4
5
6
7
8
<ng-template [ngIf]="(show$ | async)" [ngIfElse]="noshow" let-show>
<div>
Hello World {{show}}
</div>
<ng-template>
<ng-template #noshow>
No show
</ng-template>
We have added as show
which is translated into an attribute let-show
which create a template variable available in the template. let-show
doesn’t require any assignment as it is assigned the $implicit
property of the context of the template. The ng-template
is a special markup tag from Angular which will not be displayed. It defines a template to be created with a context.
The context is a regular object with a special $implicit
property. To demonstrate how ng-template
context work, we can use NgTemplateOutlet
directive:
1
2
3
4
5
<ng-container *ngTemplateOutlet="myTemplate; context: { $implicit: 'kim', city: 'Paris' }"></ng-container>
<ng-template #myTemplate let-name let-test="city">
{{name}} - {{test}}
</ng-template>
we would see kim - Paris
displayed. Just like NgIf
, the NgTemplateOutlet
is desugared:
1
2
3
4
5
<ng-template [ngTemplateOutlet]="myTemplate" [ngTemplateOutletContext]="{ $implicit: 'kim', city: 'Paris' }"></ng-template>
<ng-template #myTemplate let-name let-test="city">
{{name}} - {{test}}
</ng-template>
ngTemplateOutletContext
is an input accepting the context for the template. We see that we have set the template context as:
1
{ $implicit: 'kim', city: 'Paris' }
The $implicit
property is a special value which can be referenced from within the template using let-[name]
without assigning any value to it - the value is implicit. For other variables, we can use let-[name]="[context property]"
.
Now that we know how to use NgIf
and how it is internally represented with ng-template
, we can see how we can implement it.
NgIf
ImplementationIf we were to reimplement the NgIf
directive, we would be start by the essential functionality; display the template provided if the expression resolves to true
. Starting with this idea, we can make the following directive:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Directive({ selector: '[appIf]' })
export class AppIf {
@Input()
set appIf(condition: boolean) {
if (this.show !== condition) {
this.show = condition;
this.updateView();
}
}
// Identifies if the template is being showed.
show: boolean;
constructor(
private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef
) {}
updateView() {
this.viewContainerRef.clear();
if (this.show) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
}
}
}
In our AppIf
directive, we created an input named the same way as the selector appIf
. Doing that allows us to use the syntax *prefix="expression"
where the expression will set the input of the same name. We then use set
to intercept the input property settings where we check if the condition
resulting from the expression evaluation is true
or false
. templateRef
and viewContainerRef
are injected from the constructor. Every structural directive can inject a TemplateRef<any>
which is a reference to the template (ng-template
) where the structural directive is attached to. And every directive can inject a ViewContainerRef
which is a reference to the container where the directive is attached to.
If true
, we use createEmbeddedView
to create the template under the view container.
If false
, the view container is cleared.
This logic represents what the NgIf
does when supplied with an expression.
1
2
3
<div *appIf="show">
Test
</div>
The next functionality of NgIf
that we saw is the usage of else
keyword. The Angular directive supports defining our own keywords, and else
is no different. else
is actually defined as an input, using the following convention prefixKey
- appIfElse
:
1
2
3
4
@Input()
set appIfElse(ref: TemplateRef<any>) {
// else logic
}
The else
input takes a TemplateRef<>
as argument in order to display that particular template when the condition is false
. Therefore what we need to do is save that template, and show it when the condition changes to false
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Directive({ selector: '[appIf]' })
export class AppIf {
@Input()
set appIf(condition: boolean) {
if (this.show !== condition) {
this.show = condition;
this.updateView();
}
}
@Input()
set appIfElse(ref: TemplateRef<any>) {
this.elseRef = ref;
this.updateView();
}
elseRef: TemplateRef<any>;
show: boolean;
constructor(
private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef
) {}
updateView() {
this.viewContainerRef.clear();
if (this.show) {
this.viewContainerRef.createEmbeddedView(this.templateRef);
} else if (!!this.elseRef) {
this.viewContainerRef.createEmbeddedView(this.elseRef);
}
}
}
We are then able to use the directive as such:
1
2
3
4
5
6
<div *appIf="show; else noshow">
Test
</div>
<ng-template #noshow>
No Test
</ng-template>
We could have named else
whatever we want, all we need to do is to change the name of the input.
Following else
, then
is implemented in the same way:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Directive({ selector: '[appIf]' })
export class AppIf {
@Input()
set appIf(condition: boolean) {
if (this.show !== condition) {
this.show = condition;
this.updateView();
}
}
@Input()
set appIfElse(ref: TemplateRef<any>) {
this.elseRef = ref;
this.updateView();
}
@Input()
set appIfThen(ref: TemplateRef<any>) {
this.thenRef = ref;
this.updateView();
}
thenRef: TemplateRef<any>;
elseRef: TemplateRef<any>;
show: boolean;
constructor(
private templateRef: TemplateRef<any>,
private viewContainerRef: ViewContainerRef
) {}
updateView() {
this.viewContainerRef.clear();
if (this.show) {
this.viewContainerRef.createEmbeddedView(
!!this.thenRef ? this.thenRef : this.templateRef
);
} else if (!!this.elseRef) {
this.viewContainerRef.createEmbeddedView(this.elseRef);
}
}
}
And can be used in the same way:
1
2
3
4
5
6
7
8
9
<div *appIf="show then testThen else testElse">
Test
</div>
<ng-template #testThen>
Body
</ng-template>
<ng-template #testElse>
No test
</ng-template>
The last part we will be looking at is the variables. Template variables can either be defined using let
or as
notations which in both cases result in a different translations:
*myDir="let b"
gets translated into let-b
,*myDir="let b = a"
gets translated into let-b="a"
,*myDir="a as b"
gets translated into let-b="myDir"
,*myDir="exp as b"
gets translated into [myDir]="exp" let-b="myDir"
,*myDir="a; else b as c"
gets translated into let-c="myDirElse"
.The goal of a template variable is to make a property of the template context available within the template hence:
$implicit
property of the context into b
,a
of the context to b
,myDir
to b
,[myDir]
input to the expression and set b
to the result of the expression,myDirElse
to c
.Now that we know that, we can update our directive to include the implicit value and some extra value to demonstrate the usage of template variable bindings:
1
2
3
4
5
6
7
8
9
this.viewContainerRef.createEmbeddedView(
!!this.thenRef ? this.thenRef : this.templateRef,
{
$implicit: 'my implicit value',
extra: 'my extra value',
appIf: 'my main value',
appIfElse: 'my else value',
}
);
We can now modify our template to use the context values:
1
2
3
4
5
6
<div *appIf="show as condition; else testElse as elseVal; let impVal; let extra=extra; extra as extraAs">
Test {{condition}} - {{elseVal}} - {{impVal}} - {{extra}} - {{extraAs}}
</div>
<ng-template #testElse>
No test
</ng-template>
which would look as such when desugared:
1
2
3
4
5
6
7
<ng-template
[appIf]="show" let-condition="appIf"
[appIfElse]="testElse" let-elseVal="appIfElse"
let-impVal
let-extra="extra"
let-extraAs="extra">
</ng-template>
NgIf
GrammarNow that we have seen how to implement support for expressions, key expressions and bindings, we can look at the actual grammar defined by Angular for structural directives:
1
*:prefix="( :let | :expression ) (';' | ',')? ( :let | :as | :keyExp )*"
with the following definitions:
1
2
3
4
5
keyExp = :key ":"? :expression ("as" :local)? ";"?
let = "let" :local "=" :export ";"?
as = :export "as" :local ";"?
Where the portions are:
prefix
: HTML attribute key,key
: HTML attribute key,local
: local variable name used in the template,export
: value exported by the directive under a given name,expression
: Standard Angular expression,What we see is that a valid argument has to start by a let
binding or an expression
. Then separates the next expression with an optional separator either ;
or ,
and then allow another let
binding or a as
binding or a key expression like else
. If we take our previous example: *appIf="show as condition; else testElse as elseVal; let impVal; let extra=extra; extra as extraAs"
, we can decompose the syntax into:
show as condition;
-> expression
,else testElse as elseVal;
-> keyExp
with key: else
, expression: testElse
, local: elseVal
,let impVal;
-> local: impVal,
let extra=extra;
-> local: extra
, export: extra
,extra as extraAs
-> lcoal: extraAs
, export: extra
.In all cases, the colon :
and semicolon ;
in the notation are optional and are used to increase readability. In some cases, omitting the semicolon makes the notation look better for example for NgForOf
, let x of xs
looks better than let x; of xs
. Just like in other cases have the colon makes the syntax more readable, for example: let x of xs
is better than let x of: xs
but let x of xs; trackBy: myTrackby
is better than let x of xs; trackBy myTrackBy
as it shows explicitely that the function myTrackBy
is set on the trackBy
input.
And that concludes today’s post!
In today’s post we looked at how we could create structural directives in Angular. We explored the different functionality provided by Angular by looking in depth at NgIf
, the most commonly used structural directive. We started by looking at how we could use every features provided by NgIf
. We then moved on to look at the desugared syntax created from the structural directive. We then moved on to write our own implementation of NgIf
. And we completed the post by looking at the grammar definition of the microsyntax provided by Angular to configure structural directives. I hope you like this post and I see you on the next one!