Strongly typed ng-template in Angular

December 3rd 2021 Angular TypeScript

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.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License