Problem definition
Customizing component templates is a well-known challenge for anyone who has worked with an external UI library like PrimeNG. The capacity to redefine the entire template structure without touching the original component's source code is valuable for developers and significantly boosts reusability. Given these clear benefits, it makes sense to apply this pattern to your own projects and libraries.
Over the course of my daily work, I have encountered multiple strategies for implementing this sort of flexibility. In this write-up, I will walk through the most common ones and then present what I consider to be the strongest option. The idea is inspired by the source code of the PrimeNG library on GitHub, so credit goes to the original authors rather than to me. I will also explain the reasoning behind my preference. Let's dive in.
Scenario
Suppose we need a widget that shows user information. It should display typical fields such as first name, last name, email, and avatar. The component template might look like this:
<section class="user-box">
<div class="avatar">
<img [src]="user.avatarURL" alt="user avatar">
</div>
<div class="main">
<h4>{{ user.fullName }}</h4>
<h5>{{ user.email }}</h5>
</div>
</section>
Now imagine that we need a few distinct layouts for this tile. The underlying data stays the same, but the arrangement changes, and styling alone won't cut it. Additionally, we want the ability to supply a dedicated template for the avatar and a separate one for the remaining user details.
With that requirement in mind, let's explore the available options.
Option 1: Boolean flags
The simplest way to tackle this is by using flags. The widget component would expose one or more @Input() properties that dictate which variant of the template gets rendered. That's essentially the whole idea.
Implementation is straightforward, but there is a significant drawback: the component has to be aware of every possible usage scenario ahead of time, and all variants must be hard-coded inside its template. This rules out building generic, reusable components.
We need to keep searching.
Option 2: Content projection
Angular provides the ng-content directive for what is known as content projection. The official docs explain it thoroughly, but the short version is that it allows you to inject a fragment of markup into a component by placing it between the component's opening and closing tags. You can have several ng-content directives, each with a selector that determines which elements it captures.
Let's see how our component would look with this approach:
@Component({
selector: 'app-step02',
template: `
<section class="user-box">
<ng-content select=".avatar"></ng-content>
<ng-content select=".main"></ng-content>
</section> `
})
export class Step02Component { }
And usage would be something like this:
<app-step02>
<div class="avatar">
<img [src]="user.avatarURL" alt="user avatar">
</div>
<div class="main">
<h4>{{ user.fullName }}</h4>
<h5>{{ user.email }}</h5>
</div>
</app-step02>
Here, the upside is again the simplicity of both implementation and usage. However, there are a few downsides. Defining a default template is awkward, and repeating the custom markup on every use leads to code duplication. More importantly, the component itself has no control over the data displayed inside the projected content. It merely acts as a container for whatever is passed in. As a result, the consumer needs access to all the data that the component would otherwise manage internally. In our simple example, this is not a big deal, but think about customizing the rows, headers, and footers of a table component. In such a case, you don't want the consumer to handle data preparation or processing – that should be encapsulated inside the component. We're getting closer, but this still isn't the final answer.
Option 3: ng-template
The ng-template directive, as the name implies, encloses a template that gets inserted at one or more points during the rendering of the final DOM tree. It is not rendered where it is declared; instead, you need something like the ngTemplateOutlet directive to place it. Full details are available in the Angular documentation.
What interests us most here is that the context is configurable. This means we can decide exactly what data the template can access when it is rendered. Consequently, we can define a template for another component that references variables available only inside that component, not in the outer scope.
Sound promising? Let's revisit the flag-based idea, but this time use an @Input() property holding a reference to a template rather than a simple boolean:
@Component({
selector: 'app-step03',
template: `
<section class="user-box">
<ng-container
*ngTemplateOutlet="avatarTemplate || defaultAvatarTemplate; context: { $implicit: user }">
</ng-container>
<ng-container
*ngTemplateOutlet="mainTemplate || defaultMainTemplate; context: { $implicit: user }">
</ng-container>
</section>
<ng-template #defaultAvatarTemplate let-user>
<div class="avatar">
<img [src]="user.avatarURL" alt="user avatar">
</div>
</ng-template>
<ng-template #defaultMainTemplate let-user>
<div class="main">
<h4>{{ user.fullName }}</h4>
<h5>{{ user.email }}</h5>
</div>
</ng-template>
`
})
export class Step03Component {
@Input()
user: User;
@Input()
avatarTemplate: TemplateRef<any>;
@Input()
mainTemplate: TemplateRef<any>;
}
I won't dig into the mechanics of ngTemplateOutlet or the $implicit context variable here, as the official documentation covers all of that.
We can use the component with its default template:
<app-step03 [user]="user">
</app-step03>
Or take advantage of the customization options and define one or both templates ourselves:
<app-step03
[user]="user"
[avatarTemplate]="avatarTemplate"
[mainTemplate]="mainTemplate">
</app-step03>
<ng-template #avatarTemplate let-user>
<div class="avatar">
<!-- CUSTOM TEMPLATE -->
</div>
</ng-template>
<ng-template #mainTemplate let-user>
<div class="main">
<!-- CUSTOM TEMPLATE -->
</div>
</ng-template>
This is the pattern you'll see most often in blog posts and Angular courses. Overall, it works well – it allows for a default template, offers great flexibility, and keeps the data processing logic tucked safely inside the component.
So if it works, what's the catch?
The issue lies in how it is used. Picture a screen that brings together several components built this way. If we customize every one of them, we end up with a pile of ng-template elements with unique identifiers that must not clash. We also have to keep track of passing all those template references.
In an ideal world, everything tied to a component would live inside its own tags, requiring no extra setup. The component would automatically detect what has been provided and render accordingly, without expecting any additional parameters. I want the syntax from option 2, but with all the power described in this option.
Option 4: Merging the strengths of the previous approaches
To combine the best of both worlds, let's outline what the implementation needs to achieve. First, we should capture ng-template directives placed between the component's tags. Second, we need identifiers to tell these templates apart. Third, we must be able to render the final view, mixing data processed inside the component with the externally supplied templates.
First, we define our own directive to read the template identifier:
@Directive({
selector: '[templateId]'
})
export class TemplateWithId {
@Input('templateId')
role: string;
constructor(
public template: TemplateRef<any>
) { }
}
Next, we use the @ContentChildren() decorator to obtain a list of all elements nested inside our component. By iterating over this list, we can inspect the identifiers of the passed templates and interpret their roles accordingly:
@ContentChildren(TemplateWithId)
templates: QueryList<TemplateWithId>;
avatarTemplate: TemplateRef<TemplateWithId>;
mainTemplate: TemplateRef<TemplateWithId>;
ngAfterContentInit() {
this.templates.forEach((child: TemplateWithId) => {
switch (child.id) {
case 'avatar': {
this.avatarTemplate = child.template;
break;
}
case 'main': {
this.mainTemplate = child.template;
break;
}
}
});
}
In the code above, the ngAfterContentInit method is used. As you may know, Angular components go through a well-defined lifecycle from creation to destruction, which is thoroughly documented. Hook methods – one of which appears in this snippet – allow us to tap into specific stages of that cycle, controlling when our logic runs.
It is crucial that we do not attempt to iterate over this.templates before it is populated. This property is decorated with ContentChildren, so its value comes from outside the component – specifically from the elements passed between its tags. Any lifecycle stage prior to AfterContentInit would be too early and would likely throw an error.
Now, back to the implementation. The final piece is the ngTemplateOutlet directive, which takes the data from the component and merges it with the provided ng-template elements to produce the final rendered output. Here's the complete code:
@Component({
selector: 'app-step04',
template: `
<section class="user-box">
<ng-container
*ngTemplateOutlet="avatarTemplate || defaultAvatarTemplate; context: { $implicit: user }">
</ng-container>
<ng-container
*ngTemplateOutlet="mainTemplate || defaultMainTemplate; context: { $implicit: user }">
</ng-container>
</section>
<ng-template #defaultAvatarTemplate let-user>
<div class="avatar">
<img [src]="user.avatarURL" alt="user avatar">
</div>
</ng-template>
<ng-template #defaultMainTemplate let-user>
<div class="main">
<h4>{{ user.fullName }}</h4>
<h5>{{ user.email }}</h5>
</div>
</ng-template>
`,
})
export class Step04Component implements AfterContentInit {
@Input()
user: User;
@ContentChildren(TemplateWithId)
templates: QueryList<TemplateWithId>;
avatarTemplate: TemplateRef<TemplateWithId>;
mainTemplate: TemplateRef<TemplateWithId>;
ngAfterContentInit() {
this.templates.forEach((child: TemplateWithId) => {
switch (child.id) {
case 'avatar': {
this.avatarTemplate = child.template;
break;
}
case 'main': {
this.mainTemplate = child.template;
break;
}
}
});
}
}
Usage with the default template:
<app-step04 [user]="user">
</app-step04>
Usage with a personalized template:
<app-step04 [user]="user">
<ng-template templateId="avatar" let-user>
<div class="avatar">
<!-- CUSTOM TEMPLATE -->
</div>
</ng-template>
<ng-template templateId="main" let-user>
<div class="main">
<!-- CUSTOM TEMPLATE -->
</div>
</ng-template>
</app-step04>
Of course, the component could just as easily accept a single ng-template element. In that case, identifiers would be unnecessary and the code would be simpler.
This outcome provides all the flexibility of the previous option while being noticeably more convenient to work with. That's a win.
And as a bonus, here's the complete working example: https://stackblitz.com/edit/personalize-your-components.
