We covered every aspect of micro-syntax in the previous article. We gained a much better understanding of what Angular does when we use structural directives by examining the various parts of the official syntax reference. To reinforce this knowledge, this article will look closely at Angular's built-in NgIf and NgFor directives.
Let's start with a very common use case that most of you have probably seen in some form or another.
Following best practices, we do not manually subscribe in our component, but instead move that logic into the template. Allowing Angular's AsyncPipe to handle the heavy lifting.
We will write something like this: Let's go through the same process of identifying the various parts of this statement and looking at how Angular will translate them.
We apply the same color code:
We have the asterisk * and the ngIf
selector on the left side.
Then there's the right side. It is composed of three parts:
Let's take a closer look at each one: Because the right side begins with an expression, we know that this expression will be assigned to a '@Input()' with the same name as the selector (ngIf). When Angular translates our micro-syntax, it becomes:
<ng-template [ngIf]="(items$ | async)">...
Let's continue.
Next, we see that we export our expression into a local variable for use in our template. While this is not explicitly stated in the documentation, it is entirely possible. The mechanism enabling this is similar to the a :keyExp declaration mechanism.
To make 'items' hold the correct value, we must add the ngIf
key to our context
with the value of our @Input() ngIf
. The reason for this becomes clear when we translate our micro-syntax into the ng-template
version:
<ng-template
[ngIf]="(items$ | async)"
let-items="ngIf"
>...
For an:as declaration, Angular uses the name of the @Input()
variable as the :export or context
' key assigned to the local variable that can be used in the template. Because the @Input()
in our case is ngIf
, ngIf
must be a key in the context
, and before rendering our template, we must assign our @Input ngIf
to our context
' ngIf
key.
Finally, we have a regular :keyExp declaration that assigns the loading
TemplateRef to a @Input()
of the NgIf directive. We use our camelCase-fusing logic to determine that our input must be named ngIfElse
.
When we translate this part of our micro-syntax into the ng-template
version, we get:
<ng-template
...
[ngIfElse]="loading"
>...
Putting it all together we end up with the following:
This allows us to infer a lot of information about how the NgIf directive. Let's make some predictions about the source code and see if they hold up.
We know that the NgIf directive has:
@Input()
called ngIfngIf
key@Input()
called ngIfElseLet's check out the official NgIf source code and see if we are correct:
/**
* The Boolean expression to evaluate as the condition for showing a template.
*/
@Input()
set ngIf(condition: T) {
this._context.$implicit = this._context.ngIf = condition;
this._updateView();
}
private _updateView() {
if (this._context.$implicit) {
...
this._thenViewRef = this._viewContainer.createEmbeddedView(this._thenTemplateRef , this._context);
} else {
...
this._elseViewRef = this._viewContainer.createEmbeddedView(this._elseTemplateRef, this._context);
}
}
We see the @Input() ngIf
. Whenever the condition passed is set, the NgIf context
's implicit
and ngIf
keys are assigned our condition. Then the view is updated. When the condition passed in is truthy, the our template is rendered, else we render the _elseTemplateRef
.
/**
* A template to show if the condition expression evaluates to false.
*/
@Input()
set ngIfElse(templateRef: TemplateRef<NgIfContext<T>>|null) {
assertTemplate('ngIfElse', templateRef);
this._elseTemplateRef = templateRef;
this._elseViewRef = null; // clear previous view if any.
this._updateView();
}
This template comes from our @Input ngIfElse
, which makes sure it is a valid template and assigns it to our _elseTemplateRef
.
/**
* @publicApi
*/
export class NgIfContext<T = unknown> {
public $implicit: T = null!;
public ngIf: T = null!;
}
Finally, we can confirm that our context indeed has an ngIf
key, next to the $implicit
. Both are assigned to our truthy expression passed in through the @Input() ngIf
. That's why we can extract our async items$ using a :let declaration using the micro syntax:
*ngIf="(items$ | async); let items; else: template"
Or using a let-declaration using the ng-template
and our $implicit
key:
<ng-template
[ngIf]="(items$ | async)"
let-items>
{{items | json}}
</ng-template>
We will get the exact same results!
To see examples of everything check out this Stackblitz!
Let's move on to the NgFor directive. We assume another familiar scenario similar to the one for NgIf.
Our code will look like this:
Again, we color, divide, and conquer:
The asterisk * and the ngFor
selector make up the left side.
The right side is made up out of 3 parts:
Let's take a closer look at each one:
While the colon is frequently left out for a cleaner reading of the NgFor micro-syntax, this is actually a classic :keyExp. We assign our (items$ | async)
expression to our directive's camelCase-fused @Input() ngForOf
. This input value is also assigned to the context
' ngForOf
key, allowing us to use an :as expression to access it. We can then use our local items
variable in the template directly.
When translated to the ng-template
version, this section becomes:
<ng-template
[ngForOf]="(items$ | async)"
let-items="ngForOf"
>...
Last but not least, we have a simple :as declaration extracting the index
key of our context
into the i
variable:
<ng-template
let-i="index"
>...
Putting everything together we end up with this:
Again, we can use the info above to make some predictions about the source code and see if they hold up.
We know that the NgFor directive has:
@Input()
called ngForOf
taking in an iterable, for which each item one template gets rendered$implicit
key making the current to-be-rendered item accessible. ngForOf
key to expose the value aboveindex
key making the current to-be-rendered index accessible.Let's check out the official NgIf source code and see if we are correct:
@Input()
set ngForOf(ngForOf: U&NgIterable<T>|undefined|null) {
this._ngForOf = ngForOf;
this._ngForOfDirty = true;
}
Again, we see in the source code the @Input() ngForOf
taking in our iterable. In addition, a flag is used to indicate that this variable is dirty. This is done because NgFor does not simply render and re-render every item in our iterable every time. It actually performs some advanced calculations to determine whether a template needs to be re-rendered in order to ensure that the NgFor directive performs well even with larger lists and more complex templates.
If you want to learn more about NgFor, I recommend you look at its source code. We'll proceed to look at the context
object.
/**
* @publicApi
*/
export class NgForOfContext<T, U extends NgIterable<T> = NgIterable<T>> {
constructor(public $implicit: T, public ngForOf: U, public index: number, public count: number) {}
get first(): boolean {
return this.index === 0;
}
get last(): boolean {
return this.index === this.count - 1;
}
get even(): boolean {
return this.index % 2 === 0;
}
get odd(): boolean {
return !this.even;
}
}
Again, we find our $implicit
, ngForOf
, and index
keys. You can also see that there is a bunch of other keys available for you to access in your template!
If you are interested in seeing NgFor and it's context
in action. Check out this StackBlitz!
I can't tell you how proud I am of you if you've made it this far! I hope you now have a clearer understanding of how Angular works its magic on structural directive micro-syntax. Undoubtedly, this subject is among the most challenging aspects of the Angular framework. I hoped that by breaking things down into smaller, more manageable chunks, I would be able to provide you with the knowledge and tools you need to both better comprehend the built-in structural directives and make your own custom structural directives.
Let us pause for a moment (or two, or three) to digest all of this new information and celebrate that the asterisk has returned.
We will improve our custom "*exchangeRate" directive in the following (and possibly final?) article of this series so that it can work with the micro-syntax with ease. Additionally, instead of re-rendering the template after each successful API call, we will expose the response as an observable to our template.
Do you have any further queries or subjects you'd like me to discuss? Do you believe a video on structural directives would be beneficial? I'm interested in hearing your ideas. If you have any suggestions for future posts, please do leave a comment or send me a message.
Finally, 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.