Multiple content projections in angular
Angular content projection (with ng-content) is really useful to create reusable component. But have you ever wanted to use the ng-content inside a loop ? This will not work in angular, because angular does not support multiple ng-content tag with the same select attribute.
So let’s try to do this !
Basically, we want to tell an angular component to render multiple times a content that we provided him.
So, how can we project content in a loop ?
With ngTemplateOutlet and not ng-content 😉
The ngTemplateOutlet directive let you create embedded view form a TemplateRef. The TemplateRef represents an embedded template that can be used to instantiate embedded views
The templateRef if an HTML skeleton used to generate an HTML view.
I found the solution in the following article https://www.digitalocean.com/community/tutorials/angular-reusable-components-ngtemplateoutlet
I will explain this in my own way in this article 😀
Simple multiple projections
Let’s create a simple multiple projections.
We need to create our container component that will loop through a list to render the content multiple times.
Let break it down.
- We need a list of item in the component to loop through it.
- The @ContentChild is used to get the content of the container component. It give use the skeleton to generate the HTML view.
- In the HTML, we loop through the item. For each element we generate the HTML view from the templateRef we accessed with the @ContentChild To do this, we passe the templateRef to the [ngTemplateOutlet] directive.
- With [ngTemplateOutletContext] we can provide a context to the view. We can use the item string in the template definition when we call the SimpleContainerComponent. We set the $implicit attribute to our item to retrieve it in the view.
Now let call this component with the template
Here we call the container with a list of string. We provide the template (with ng-template) to tell the container how to render each fruits. This template will be passed to the [ngTemplateOutlet] via the @ContentChild.
The let- syntax let us get the context defined by [ngTemplateOutletContext]. The let-item defines a variable named item that we can use in our template. In this example it is a string (because we set a string the $implicit attribute in the [ngTemplateOutletContext]) and with this we can dispaly the fruits name.
Et voilà 🍾! This is how you use content projection in a loop !
Here is the working example in stackblitz
Note that the [ngTemplateOutlet] is the long syntax for structural directive and it is equivalent to the following (see https://angular.io/guide/structural-directives#microsyntax for more info)
Advanced multiple projections
This was the simpliest example for multiple projections. Let do a more complex container component.
Let’s say we want to create a dashboard where we will display multiple columns. Each column is a category and will have its header and a list of items. We want it to look like that
The container willl work with an object that contains the categories and the items inside. Here are the objects’ structure.
export interface Category {
name: string;
items: AdvancedItem[];
}
export interface AdvancedItem {
id: number;
title: string;
}
// generic type for the view context
export interface ViewContext<T> {
$implicit: T;
}
// expected type for the context of item
export interface AdvancedItemViewContext extends ViewContext<AdvancedItem> {
first: boolean;
last: boolean;
}
The container must receive an array of Category. And for the each item we want to let the developer being able to know if the item is the first or the last one. So we create a specific AdvancedItemContext. This will be the format of the item context variable for the view.
We also want the developer to be able to provide the template for the header and for the items. So in this case we have to provide 2 different templates to the container. And the container must know which template is the header and which is the item.
We can’t do the template definition the same way we did in the simple example because the container wouldn’t know which template is which.
We have to differentiate them. And to do this we can use 2 directives, let’s create them 🙌
These directives are empty are just needed to retrieve the header and item templateRef and to define the context type (we will come back later for this).
Let’s define our new container.
Here we changed the @ContentChild to get the header and item template separately. 2 information are important in the @ContentChild
- The first parameter is the reference of the directive. With this, angular get the correct template.
- The second parameter : {read: TemplateRef} tells angular to get the template of the directive and not the directive instance itself. We could have retrieved the templateRef by getting the directive instance and then access the templateRef injected variable (like myDir.templateRef). But accessing the templateRef directly in the @ContentChild is clearer.
Note that there is 2 methods getHeaderContext and getItemContext to get the context based on looped item. This is useful to have a strong typed component because we can’t typed the context variables in the HTML.
The getItemContext not only returns the item but also first and last boolean, so in the template we will be able to use those boolean.
That’s all, we can now call our container 🙂
We can simply access the item by using ‘let item’. This work because the item was set in the $implicit attribute of the view context. The ‘let item’ is equivalent to ‘let item = $implicit’.
We can access the first boolean using the ‘let first = first’ (like in the *ngFor). This is because we define the first attribute in the context. This is the same for the last.
Et voilà 🍾! We can now use this container by defining any template for the headers and the items.
Here is the working example in stackblitz
Structural directive type
Note that in the directive component, we defined the type of the context. For the AdvancedContainerItemDirective, we set the context type to AdvancedItemContext.
constructor(private templateRef: TemplateRef<AdvancedItemViewContext>) {}
So in your IDE (at least in webstorm) you should have an autocompletion for the context variable ! 😁
Using NgForOfContext
Our provided context is really similar to the *ngFor context(with the first and last boolean). Actually, we can directly use the same context : NgForOfContext.
This is a simple class with this constructor :
constructor($implicit: T, ngForOf: U, index: number, count: number);
We change out getHeaderContext method to return a NgForOfContext
And that’s it 🍾! We can now have access to index, first, last, even and odd variable in our view context !
You can check out the NgForOfContext here : https://github.com/angular/angular/blob/master/packages/common/src/directives/ng_for_of.ts
Others ideas
You could also provide a default template defined in the advanced-container.component.html and let the developer override this default template if he wants to.
That’s it for this article😀, here is the link to the github with the two examples: https://github.com/BobBDE/multiple-content-projection