Strongly typed ng-template in Angular
The ng-template
element in Angular allows you to reuse parts of a component template, which in a sense makes it a lightweight subcomponent. By default, its context is not typed, but it can be made strongly typed with a little trick.
The *ngTemplateOutlet
structural directive can be used to refer to an ng-template element that exists in the same component template:
<ng-container *ngFor="let topItem of topItems">
<ng-container
*ngTemplateOutlet="itemTemplate; context: { ctxItem: topItem }"
></ng-container>
</ng-container>
This replaces the ng-container
element with the contents of the ng-template
element referenced by the itemTemplate
template variable. The context
is used to pass it a custom context that it can use. In this case, the context consists of a single field called ctxItem
that contains the current value of the topItem
loop variable.
The referenced ng-template
element can be defined anywhere in the component template:
<ng-template #itemTemplate let-item="ctxItem">
<div>{{ item.id }}: {{ item.label }}</div>
</ng-template>
The #itemTemplate
template variable must be declared on the ng-template element so that it can be referenced elsewhere, as seen in the *ngTemplateOutlet
snippet above.
The let-item="ctxItem"
attribute is used to assign the context passed by *ngTemplateOutlet
to a variable that is only accessible within this ng-template
element. In this case, the ctxItem
context field is assigned to the item
variable. This variable is then used within the element by interpolation. However, the variable has type any
, which means that no type checking can be done during compilation. If you make a typo, you can only detect it at runtime.
This can be quite inconvenient, especially if the value is a complex object. Fortunately, there is a way to make the variable strongly typed with a little trick.
First, you need a method in the component that takes an argument of your type and returns it as is:
toItem(item: Item): Item {
return item;
}
If you call this method with a value of type any
, it will return a value of the correct type - Item
in our case. We can take advantage of this by calling the method from the template and assigning the resulting value to another variable.
This can be done using an *ngIf
structural directive. To avoid extra markup, we can use it in an ng-container
element:
<ng-template #itemTemplate let-item="ctxItem">
<ng-container *ngIf="toItem(item); let item">
<div>{{ item.id }}: {{ item.label }}</div>
</ng-container>
</ng-template>
The output of the toItem
method is assigned to the item
variable, which shadows the variable declared in the ng-template
element with the same name. We could use a different name to avoid this, but in this case it is convenient to use the same name as this prevents the untyped variable from being used. Instead, the newly declared item
variable, which is correctly typed as Item
, is used. Now, if you make a typo when referring to one of the fields, the error will be detected during AOT template compilation.
A working example using this technique can be found in my GitHub repository.
The ng-template
element can be used in combination with the *ngTemplateOutlet
structural directive to reuse parts of a component template within that template. This can be error-prone because the variables declared to access the context passed in ng-template
are not strongly typed. The problem can be fixed by declaring another variable with the same name and assigning it the same value, which is cast to the correct type using a component helper method.