Angular structural directives and their microsyntax
Angular structural directives and their microsyntax
Have you ever wondered what's that star prefix for *ngIf
and *ngFor
? That's called a structural directive.
In this article, I'll show you what it is when you would want it and how it works.
I’ll also do a part 2, showing you how to create your own structural directives.
Templates are the structure
Let’s start defining what it is.
A structural directive is a directive with a structure. The structure is an ng-template.
When you write <div><p>Text</p></div>
, you’re telling Angular to “_declare the structure of a div tag, with a paragraph tag, with the string “Text”, and render it_”.
But when you wrap it in an <ng-template><div><p>Text</p></div></ng-template>
, you’re telling Angular to “_declare the structure of a div tag, with a paragraph tag, with the string “Text”_”. But notice that now we’re not telling Angular to render it.
Now, put a directive in the <ng-template>
and you have a structural directive:
<ng-template [ngIf]=“condition”><div><p>Text</p></div></ng-template>
Synthetic sugar
That's how ngIf works. Angular parses the <ng-template>
, generating a TemplateRef, which is injected in the NgIf directive. If the condition passed to ngIf is true, the template is rendered.
But it would be very annoying to create an ng-template every time we wanted to use NgIf or any other directive that requires an ng-template. So the Angular team created synthetic sugar. Like a shortcut.
When you prefix your directive with a star, Angular wraps it in an ng-template and applies the directive to the ng-template. So <div *ngIf=“condition”>Abc</div>
, becomes <ng-template [ngIf]=“condition”><div>Abc</div></ng-template>
It’s just synthetic sugar. You could write your whole app without the star prefix if you wanted.
Only one allowed
Knowing how it works, you can now understand why we can only use one structural directive per element. If you were to use *ngIf
and *ngFor
in the same element, how would Angular desugar that? ngIf first and then ngFor? The reverse? Both in the same template?
Microsyntax
Talking about ngFor, it seems much more complicated than ngIf, right? I've seen some really complex ngFor expressions, like passing a trackBy function, piping an observable array, grabbing the index, and checking if it’s the last element.
<div *ngFor="let item of list$ | async; trackBy: trackByFn; let itemIndex = index; let islast = last">{{ item }}</div>
TypeScript<div *ngFor="let item of list$ | async; trackBy: trackByFn; let itemIndex = index; let islast = last">{{ item }}</div>
Initially, I thought that was a ngFor-specific lingo, but it's not. It's a fully documented syntax that works for any structural directives, even ones that you end up creating. It's called the "structural directive microsyntax". (kinda obvious)
The structural directive microsyntax divides expressions by semicolons (;). In our NgFor example, we'd have 4 expressions:
- let item of list$ | async
- trackBy: trackByFn
- let itemIndex = index
- let islast = last
Declarations
Expressions starting with let
are variable declarations. You declare the variable name right after let
and use the equal sign (=) to point it to the name of the variable in the exported directive context.
That was a lot, sorry.
What I mean is that when we render an <ng-template>
, we can optionally pass a context object. And the properties of this context object are passed to the template. The context object can have multiple explicit variables and a single implicit variable.
<!-- Rendering an <ng-template> with a context object -->
<ng-container *ngTemplateOutlet="templateExample; context: { $implicit: 'test', index: 1 }"></ng-container>
<!-- Using the context properties in the <ng-template> -->
<ng-template #templateExample let-itemIndex="index" let-item>
<p>#{{ itemIndex }} - {{ item }}</p>
</ng-template>
TypeScript<!-- Rendering an <ng-template> with a context object -->
<ng-container *ngTemplateOutlet="templateExample; context: { $implicit: 'test', index: 1 }"></ng-container>
<!-- Using the context properties in the <ng-template> -->
<ng-template #templateExample let-itemIndex="index" let-item>
<p>#{{ itemIndex }} - {{ item }}</p>
</ng-template>
It's like a JavaScript function, we have the parameters, which we declare and thus are very explicit, and we have this
which is an implicit variable that exists even though we haven't declared it.
function example(itemIndex, isLast) {
// Explicit
console.log(itemIndex, isLast);
// Implicit
console.log(this);
}
TypeScriptfunction example(itemIndex, isLast) {
// Explicit
console.log(itemIndex, isLast);
// Implicit
console.log(this);
}
In a function, you can have as many parameters as you want, but only one this
. Just like that, in an ng-template, you can have as many explicit variables as you want, but only one implicit variable.
The implicit variable is what you get when you don't point to any exported variable. let item
for example, is getting the implicit variable. But let isLast = last
is getting the explicit last
variable and let itemIndex = index
is getting the explicit index
variable.
After desugaring the variables, that's what we get:
<ng-template let-item let-itemIndex="index" let-isLast="last">
<p>#{{ itemIndex }} - {{ item }}</p>
<p *ngIf="isLast">The end</p>
</ng-template>
TypeScript<ng-template let-item let-itemIndex="index" let-isLast="last">
<p>#{{ itemIndex }} - {{ item }}</p>
<p *ngIf="isLast">The end</p>
</ng-template>
Key expressions
Expressions with two arguments and an optional colon (:) between them are key expressions. The expression (in the right) gets assigned to the key (in the left) with a prefix before it.
Let's look at some examples.
In \*ngIf="condition; else otherTemplate
, for the else otherTemplate
expression:
- ngIf is the prefix
- else is the key
- otherTemplate is the expression
That gets desugared to <ng-template [ngIfElse]="otherTemplate"></ng-template>
In *ngFor="let item of list; trackBy: trackByFn
, for the trackBy: trackByFn
expression:
- ngFor is the prefix
- trackBy is the key
- trackByFn is the expression
That gets desugared to <ng-template [ngForTrackBy]="trackByFn"></ng-template>
Also, for that NgFor example, of list
in let item of list
is ALSO a key expression.
- ngFor is the prefix
- of is the key
- list is the expression
That gets desugared to <ng-template [ngForOf]="list"></ng-template>
Local bindings
The last thing to mention is the optional as
keyword at the end of the expression. It declares a template variable and maps the result of the expression to it.
*ngIf="condition as value"
becomes <ng-template [ngIf]="condition" let-value="ngIf">
Conclusion
That's it. You now understand how structural directives work and how to analyze their microsyntax.
I'll do another article on how to code a custom structural directive from scratch and how to tell the Angular compiler to type-check its context.
Have a great day and see you soon!