In the previous article of this series, we took a closer look at the context
object and how we can use it to expose all kinds of data and even functions to our template. Combined with @Input()
s and dependency injection, we got a first glimpse of how powerful structural directives are once they can interact with the rest of our application or even the outside world (using Angular's HttpClient
.)
While I quickly "introduced" the structural directive micro-syntax in the first article, we have applied all our custom directives directly to ng-template
s. Now it is time to reintroduce the asterisk and take the time to understand how Angular interprets this shorthand syntax.
One structural directive per element
Before we begin exploring the ins and outs of Angular's micro-syntax, I should note that we can only ever apply one structural directive per host element. As the official documentation states: The reason is simplicity. Structural directives can do complex things with the host element and its descendants. When two directives lay claim to the same host element, which one should take precedence?
Therefore, if we add multiple structural directives to the same element the Angular compiler will throw an error.
Understanding the syntax reference
I spent a lot of time trying to figure out the best way to introduce Angular's micro syntax.
Should I do it the same way I picked up structural directives. I got gradually introduced to more and more bits, pieces, and gotcha's as I used structural directives. However, I always lacked a deeper, more complete understanding of the underlying mechanisms that make them work.
Or should I start with the official syntax reference, which is intimidating to say the least. I always found that part of the Angular documentation to be one of the most confusing. However, once I truly wrapped my head around it, it was like an epiphany and absolutely changed the way I look at and understand structural directives.
Therefore, I decided to start with that: The official syntax reference. I will do my best to break things down and explain every piece of it. Together, we will tackle it and take our Angular skills to the next level.
We got this!
Without further ado: the structural directive syntax reference
When I first saw this expression in the documentation, I had no idea any of this meant. I was overwhelmed to be completely honest. But equipped with the knowledge from our previous articles, some clever coloring, and breaking this monstrosity down into smaller pieces, we will demystify this syntax and understand what is going on.
Adding colors makes the distinct parts more obvious.
Much better! I also want to flip the first :let and :expression. The reason will become clear a little later.
Awesome! This already looks a lot less intimidating.
Components of the structural directive syntax reference
Let's dive in and start breaking the syntax down into its sub-components.
The equal sign divides the expression into two sides:
1. *:prefix
The asterisk * and the :prefix make up the left side.
The asterisk instructs Angular to encapsulate this directive in a ng-template
.
:prefix is always the directive's selector. If the directive has an input with the same name as our :prefix AND the right side starts with an :expression, it can also serve as the first input of our directive.
I will explain this in detail in the section below.
2. (:expression | :let) (';' | ',')? (:let | :as | :keyExp)*
The right side is made up of several subgroups. A logical group is denoted by a pair of blue parentheses. The blue vertical line | indicates a logical OR within a group. If the group is followed by a blue question mark ?, it is optional. If it is followed by a blue asterisk *****, the group can be repeated multiple times. (This includes 0 times.)
Again, we can divide the right side into 3 smaller parts:
1. (:expression | :let)
This first part is either an :expression or a :let declaration.
1.1 :expression
If we have an input parameter with the same name as the selector in our directive, this part must always be a :expression or Angular will throw an error.
Any valid Angular expression is regarded an :expression. This includes boolean (true/false) values, function calls, and component variables.
The following example demonstrates how the micro-syntax binds the :expression to the :prefix input of the component:
@Directive({
standalone: true,
// our directives selector, which becomes the :prefix
selector: '[myNgIf]',
})
export class MyNgIfDirective {
// dependencies needed to render
private _vcr = inject(ViewContainerRef);
private _template = inject(TemplateRef);
private _isRendered = false;
@Input()
// an input with the same name as the selector
// binding the :expression to the :prefix (myNgIf)
set myNgIf(expression: boolean) {
this._isRendered = expression;
if (this._isRendered) {
this._vcr.createEmbeddedView(this._template);
} else {
this._vcr.clear();
}
}
}
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, MyNgIfDirective],
template: `
<button (click)="isRendering = !isRendering">Toggle</button>
<p>We can now bind an Angular expression like our component variable isRendering ({{isRendering}}) to our myNgIf input.
If isRendering is true we will see our component rendered below:
<div *myNgIf="isRendering">Rendered.</div>
`,
})
export class App {
public isRendering = false;
}
If no @Input()
property matches the selector of the directive, this first part must be a :let declaration.
1.2 :let declaration
In the previous article, we saw let declarations applied directly to the ng-template
. We learned how they extract the variables from our context
and make them available for use in our template.
The micro-syntax :let declarations accomplish the same thing.
Their structures are very similar and look like this:
let local=“export ';'?”
- local is the local variable name used in the template.
-
export refers to the value exported by the directive under a specific name, which is one of the keys in our
context
object! Except when the key is$implicit
. This key is unique and is assigned to 'let local' by default. If we don't include ="export" in our let declaration, the value of$implicit
is automatically assigned to our local variable. - ';'? is an optional semi-colon that indicates the end of the let declaration.
An example of the micro-syntax :let declaration:
@Directive({
standalone: true,
selector: '[myLet]',
})
export class MyLetDirective implements OnInit {
// dependencies needed to render
private _vcr = inject(ViewContainerRef);
private _template = inject(TemplateRef);
public ngOnInit() {
this._vcr.createEmbeddedView(this._template, {
$implicit: { hello: 'World from implicit' },
second: { hello: 'World from second' },
});
}
}
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, MyLetDirective],
template: `
<div *myLet="let ctx;">
<p>{{ctx | json}}</p>
</div>
<div *myLet="let secondCtx = second">
<p>{{secondCtx | json}}</p>
</div>
`,
})
export class App {}
As you can see we can extract both the $implicit
into the ctx
template variable and the second
key into the secondCtx
variable using our :let declaration. Take note that we added a semi-colon at the end of the first declaration. This is not required. The second declaration without it also works.
2. (; | ,)?
We can add an optional semi-colon or comma after our initial :expression or :let declaration to separate it from the following component. This can be used to make your code more readable. It is not necessary.
Let us update our simple directive from the:let declaration example to demonstrate that we can, but are not required to, use semi-colons and commas to separate our :let declarations.
@Directive({
standalone: true,
selector: '[myLet]',
})
export class MyLetDirective implements OnInit {
// dependencies needed to render
private _vcr = inject(ViewContainerRef);
private _template = inject(TemplateRef);
public ngOnInit() {
this._vcr.createEmbeddedView(this._template, {
$implicit: { hello: 'World from implicit' },
second: { hello: 'World from second' },
});
}
}
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, MyLetDirective],
template: `
<div *myLet="let context; let secondCtx = second">
semi-colon
<p>{{context | json}}</p>
<p>{{secondCtx | json}}</p>
</div>
<div *myLet="let context, let secondCtx = second">
colon
<p>{{secondCtx | json}}</p>
<p>{{secondCtx | json}}</p>
</div>
<div *myLet="let context let secondCtx = second">
nothing
<p>{{context | json}}</p>
<p>{{secondCtx | json}}</p>
</div>
`,
})
export class App {}
To make our code clearer, we can add a semicolon or even a comma between our :let statements. If we exclude these, our code still functions as intended.
3. (:let |:as | :keyExp)*
3.1 :let declaration
This third section may be repeated as many times as needed following the initial :expression, :let declaration, or the optional semicolon or comma. It can either be a :let, :as, or :keyExp declaration. The :let declaration is the same as the one we used above. We move on to the :as declaration.
3.2 :as declaration
Similar to the :let declaration, the :as declaration extracts context
variables and binds them to the template.
It's syntax is very similar to our :let declaration.
It looks like this:
export as local ';'?
-
export is the value exported by the directive under a given name, aka one of the keys in our
context
object! - local is the local variable name used in the template.
- ';'? is an optional semi-colon that indicates the end of the let declaration.
One could say, that :as is pretty much :let in reverse order. Angular actually translates both expression to the same "simple" let declaration: let-local="export" on the ng-template
.
We can see this in action rewriting our AppComponent to use an :as declaration instead of the second :let declaration:
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, MyLetDirective],
template: `
<div *myLet="let context; second as secondCtx">
nothing
<p>{{context | json}}</p>
<p>{{secondCtx | json}}</p>
</div>
`,
})
export class App {}
With the: as an d:let declarations, we now have two tools to extract context
data for usage in our template. If a property of our directive has the same name as our selector, we also know how to assign a @Input()
value to it. We frequently need to give our directive multiple inputs. See how :keyExp declarations do this.
3.3 :keyExp declaration
It's syntax is very similar to our :let declaration.
It looks like this:
key ":"? :expression (as local)? ';'?
-
key ":"? indicates an assignment to a special
@Input()
variable of our directive. Why special? The literal key value and the directive's selector, or the :prefix, are combined to form the variable's name. The key is appended to the end of the :prefix in camelCase. :prefix and key become :prefixKey, ngIf and else become ngIfElse. You can see this in the Angular source code. Also, you can see that the colon to indicate an assignment (similar to assigning a value to a key in a JavaScript object) is optional.*ngIf="loaded; else loadingTemplate"
and*ngIf="loaded; else: loadingTemplate"
are the same thing. -
:expression is the Angular expression bound to your :prefixKey
@Input()
- (as local)? is an optional way to directly use the :expression, which was passed in as an
@Input()
, in the template with the local variable name. Very important: For this to work, the directive'scontext
object must have a camelCase-fused-key. A directive like this:<div *calculateAvg="let avg; data: (data$ | async) as testData">...</div>
Would need to have acalculateAvgTestData
key in itscontext
object fortestData
to expose the correct value. - ';'? is an optional semi-colon that indicates the end of the let declaration.
Let's look at the full source code of the *calculateAvg
example from above:
@Directive({
// directive to calculate the average
selector: '[calculateAvg]',
standalone: true,
})
export class CalculateAvgDirective {
// dependencies needed to render the template
private _vcr = inject(ViewContainerRef);
private _template = inject(TemplateRef);
// our average that we calculate whenever
// new data is provided
private _avg = 0;
// private reference to our data so we can calculate the
// average in the set function
private _data = [];
@Input()
// we want to be able to use the key data in our key expression
// we know that we need to camelCase-fuse our key with the directive name
// calculateAvg (:prefix) + data (key) => calculateAvgData (:prefixKey)
set calculateAvgData(values: number[]) {
// store data passed in
this._data = values;
const sum = this._data.reduce((a, b) => a + b, 0);
// calculate the average
this._avg = sum / Math.max(1, values.length);
// render template
this._vcr.createEmbeddedView(this._template, {
// make avg available through $implicit
$implicit: this._avg,
// make data available for as expression
calculateAvgData: this._data,
});
}
}
@Component({
selector: 'my-app',
standalone: true,
imports: [CommonModule, CalculateAvgDirective],
template: `
<div *calculateAvg="let avg; data: (data$ | async) as testData">
micro-syntax: {{avg}} is the average of {{testData}}
</div>
<ng-template
calculateAvg
[calculateAvgData]="(data$ | async)"
let-testData="calculateAvgData"
let-avg
>
ng-template: {{avg}} is the average of {{testData}}
</ng-template>
`,
})
export class App {
data$ = of([1, 3, 3, 1, 4, 0, 2, 2, 1, 3]);
}
I highly recommend to examine working code is here.
Don't worry if a lot of information sounds confusing or still a bit unclear. :keyExp declarations are definitely the hardest part to understand about structural directives and their micro-syntax.
What's next
Today, we've covered every aspect of micro-syntax. We dissected and thoroughly examined the official syntax reference. I hope this has helped you understand what Angular does when structural directives are used in your applications. However, I believe that in order to cement today's knowledge, we should look at how the syntax is used in Angular's built-in directives. In the following article, we will apply what we've learned today and dissect the NgIf and NgFor directives.
If you liked this article feel free to like and share it with others. If you enjoy my content follow me on Twitter or Github.
Top comments (4)
A great document on structural directives, but I still have a question about one thing. What does the colon mean that's in front of 'let' and 'expression'?
All it means is that it’s a placeholder for the respective parts of code.
For :expression that means:
Any valid Angular expression is regarded an :expression. This includes boolean (true/false) values, function calls, and component variables.
For :let that means:
The micro-syntax :let declarations accomplish the same thing.
Their structures are very similar and look like this:
let local=“export ';'?”
So a better way to frame this is that the preceding colon indicates a variable and let and expression are the names of the variables
Thank you so much for answer. This document helped me a lot in getting grasp of structural directives and how it works.
Awesome! Let me know if there’s any other topic you’d like me to explore! Always looking for new ideas!