Understanding The Limitations Of Template Syntax Desugaring In Angular 7.0.4
CAUTION: The conclusions in this post are based on the results of experimentation - they are not based on documentation. Nor are they based on logic that I can find in the Angular source code.
Last week, I looked at the fact that the Async Pipe "as" syntax is nothing more than the "$implicit" view context property provided by the current directive (which turns out to not be exactly true, as demonstrated below). In that particular post, I used the NgIf directive as the exploratory context. But, this got me thinking about how other Angular directives can be rewritten to use the alternative syntax. The next one I tried was NgFor. I did get NgFor working with and without the "as" syntax; but, it showed me that there are some limitations in the way the "*" template syntax can be "desugared" into the "ng-template" syntax in Angular 7.0.4.
To see what I mean, consider the following ng-template version of the NgFor directive:
<ng-template ngFor [ngForOf]="values">
x
</ng-template>
In this case, all we're doing is looping over the "values" array without actually binding to any of the context variables. It will output the "x" for as many elements as there are in the collection.
Now, if we want to "sugar" this syntax down, we think we could try the following:
<!-- CAUTION: DOES NOT WORK. -->
<div *ngFor="of values">
x
</div>
If you remember from my earlier post (2-years ago) about Structural Directives, each of the operators in the "*" syntax gets concatenated with the attribute name. So, in this case, we would expect the "of" to be desugared into "ngForOf", which would mirror our "ng-template" syntax. However, if we try to run this code, we get the following compile-time error:
ERROR in : Can't bind to 'ngFor' since it isn't a known property of 'div'.
Property binding ngFor not used by any directive on an embedded template. Make sure that the property name is spelled correctly and all directives are listed in the "@NgModule.declarations".
It seems that there is a limitation to what can be in the "first slot" of a "*" template. From the NgIf directive, we know that the first slot can be the expression bound to the primary input of the structural directive. But, clearly, in our NgFor example, the first slot can't be an expression bound to a secondary input of the structural directive. It turns out that secondary input bindings have to be in or after the "second slot" of the "*" template.
To demonstrate this, we can fill-in the first slot of our *ngFor directive with a throw-away binding to the NgForContent object:
<div *ngFor="let _ = first ; of values">
x
</div>
This works! Once the first slot of the template has our throw-away template-local variable, the "of values" properly binds to the NgForOf structural directive input.
Once we get past the first slot, the placement of the "of" short-hand doesn't seem to matter. For example, this also works:
<div *ngFor="let isFirst = first ; let isLast = last ; of values ; let isEven = even">
x
</div>
Here, the "of values" is mixed-in with various template-local bindings.
Now that we understand this limitation of the "*" template desugaring, let's revisit the "as" syntax. If you recall from the previous post, the "as" syntax really has nothing to do with the Async Pipe. As such, the following templates all do the same thing:
<ng-template ngFor [ngForOf]="values" let-collection="ngForOf">
{{ collection }}
</ng-template>
<div *ngFor="let collection = ngForOf ; of values">
{{ collection }}
</div>
<div *ngFor="let _ = first ; of values as collection">
{{ collection }}
</div>
In each of these, we're saving the values reference into a template-local variable named "collection". In the last example, we need to use a throw-away binding in the "first slot" due to the limitations explained above.
Now, in my previous post, I said that the "as" syntax is just a binding to the "$implicit" context property. This turns out to not be true all the time. In the cast of NgIf, it is true; but, that is by coincidence. In reality, the "as" syntax is just a short-hand binding to whatever the contextual input property is.
What this means is that in the above example, the "of values as collection" is a short-hand binding to the contextual "NgForOf" property, not to the "$implicit" property.
With that said, let's take a look at how this syntax works with the Async Pipe:
<ng-template ngFor [ngForOf]="valueStream | async" let-collection="ngForOf">
{{ collection }}
</ng-template>
<div *ngFor="let collection = ngForOf ; of ( valueStream | async )">
{{ collection }}
</div>
<div *ngFor="let _ = first ; of ( valueStream | async ) as collection">
{{ collection }}
</div>
These all do the same thing. Which hopefully underscores the fact that the "as" syntax has nothing to do with the Async Pipe itself.
When it comes to the "*" template syntax and the desugaring into the "ng-template" syntax in Angular 7.0.4, there appears to be one major caveat: the "first slot" of the template has to either have a template-local binding or the expression bound to the primary structural directive input. Once you understand this caveat, the syntax becomes very flexible, especially with regard to template binding short-hands like "let" and "as".
Want to use code from this post? Check out the license.
Reader Comments