The Basics of Implementing Content Projection in Angular with ng-template
Enhancing component reusability by sharing template references with child components.
In a previous blog post, I showed how to achieve basic content projection in Angular using ng-content
. In this article, I'll discuss another method: using a TemplateRef
with a component for conditional rendering in more dynamic situations.
What is a TemplateRef
?
While <ng-content>
works well for straightforward projection scenarios, Angular offers a method to pass templates as component input references using TemplateRef
. A TemplateRef
is an embedded template that enables the creation of embedded views. The <ng-template>
directive not only lets us define an embedded view that we can refer to programmatically but also allows us to dynamically provide context to it. This makes it possible to use this embedded view as an input parameter.
First, we need to understand how using the ng-template
directive allows us to define a TemplateRef
in our template that we can then reference and render.
<ng-template #exampleTemplate>
<p>This is an example template. We have only defined it here.</p>
<p>Simply defining this here will NOT render this template.</p>
<p>We can access this TemplateRef with the name exampleTemplate</p>
</ng-template>
<!-- Use the ngTemplateOutlet structural directive to render the template -->
<ng-container *ngTemplateOutlet="exampleTemplate"></ng-container>
In the above code block, we define a template using the ng-template
directive. The template syntax #exampleTemplate
allows us to name this template and later access a reference to it, which is of type TemplateRef
.
We then use the ng-container
directive along with the ngTemplateOutlet
structural directive to render an instance of the template into the DOM. Note that the ng-container
directive does not render a DOM element itself, which is why it is typically used with structural directives like *ngTemplateOutlet
and *ngIf
to add conditional rendering logic inside of Angular views.
Understanding Template Context Data
In the example above, we've only shown how to render static content. However, we can design our templates to accept context data. This data can be injected at the time of rendering through a context parameter.
Context data can be passed either implicitly or explicitly.
Implicit Context Passing
import { Component } from '@angular/core';
@Component({
selector: 'app-implicit-context-example',
template: `
<ng-template #myTemplate let-name>
Hello, {{ name }}!
</ng-template>
<ng-container *ngTemplateOutlet="myTemplate; context: {$implicit: 'John'}"></ng-container>
`
})
export class ImplicitContextExampleComponent {}
In this implicit example, the context data ('John'
) is passed without naming it explicitly. The let-name
syntax in the ng-template
declaration allows us to use the context data ($implicit: 'John'
) inside the template. The $implicit
keyword is used to pass the value directly.
Explicit Context Passing
import { Component } from '@angular/core';
@Component({
selector: 'app-explicit-context-example',
template: `
<ng-template #myTemplate let-greeting="greetingText" let-name="userName">
{{ greeting }}, {{ name }}!
</ng-template>
<ng-container *ngTemplateOutlet="myTemplate; context: {greetingText: 'Hello', userName: 'Jane'}"></ng-container>
`
})
export class ExplicitContextExampleComponent {}
In the explicit example, the context data is passed with explicit names (greetingText: 'Hello', userName: 'Jane'
). In the ng-template
, we declare local variables (let-greeting="greetingText"
and let-name="userName"
) that map to the keys in the context object provided to *ngTemplateOutlet
. This way, we can access the context data inside the template using the names we've defined.
Example Usage: Providing ng-template as an input
Let's create a simple Angular example to show how to display context-specific user data in a modal by using an ng-template
with context. This example will dynamically show the remaining onboarding tasks that the user needs to finish.
Parent Component (parent.component.ts):
import { Component } from '@angular/core';
interface Task {
description: string;
completed: boolean;
}
@Component({
selector: 'app-parent',
template: `
<ng-template #userTasksTemplate let-tasks="tasks">
<div *ngFor="let task of tasks">
<p *ngIf="!task.completed">{{task.description}}</p>
</div>
</ng-template>
<app-modal *ngIf="tasksRemaining()" [contentTemplate]="userTasksTemplate" [context]="{tasks: userTasks}"></app-modal>
`
})
export class ParentComponent {
userTasks: Task[] = [
{ description: 'Verify email', completed: false },
{ description: 'Complete profile', completed: false },
{ description: 'Set up two-factor authentication', completed: false }
];
tasksRemaining(): boolean {
return this.userTasks.some(task => !task.completed);
}
}
The ParentComponent
creates a list of tasks, each with a description and a completion status. It uses an ng-template
to determine how to display these tasks. The component also has logic to show a modal component if there are any incomplete tasks, sending the tasks data to the modal to dynamically show them based on their completion status.
This code illustrates how a parent component can pass a template reference (#userTasksTemplate
) along with a context ({tasks: userTasks}
) to a child modal component.
Modal Component (modal.component.ts):
import { Component, Input, TemplateRef } from '@angular/core';
@Component({
selector: 'app-modal',
template: `
<div class="modal">
<ng-container *ngTemplateOutlet="contentTemplate; context: context"></ng-container>
</div>
`,
styles: [`
.modal {
border: 1px solid #ccc;
border-radius: 5px;
padding: 20px;
width: 300px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
`]
})
export class ModalComponent {
@Input() contentTemplate!: TemplateRef<any>;
@Input() context!: any;
}
The ModalComponent accepts contentTemplate
and context
as input parameters, which are provided from the ParentComponent
usage of the modal.
The modal component then uses ngTemplateOutlet
to render the content dynamically, displaying the user's remaining onboarding tasks.
Use Cases for Rendering Dynamic Templates
TemplateRef
can be used to instantiate templates based on runtime conditions or data. This is especially helpful when the content needs to dynamically change after the component is initialized.
In order to understand when we might choose to render a template over using <ng-content>
, let's run through situations in which you may need more dynamic control over the projection that is rendered.
Here are specific, real-world use cases that show when choosing a TemplateRef
input could be more beneficial than using <ng-content>
:
1. Customizable Data Grids
Use Case: Implementing a data grid component where each cell's content can vary significantly, from simple text to complex forms or charts based on the data type.
WhyTemplateRef
? By accepting TemplateRef
s for cell templates, you can provide a high level of customization for each cell type (e.g., currency, dates, dropdowns) while keeping the grid component generic and reusable. Users of the grid component can define how each type of cell should be rendered depending on the data it represents, something that's cumbersome to achieve with <ng-content>
due to its static nature.
2. Dynamic Lists with Custom Item Templates
Use Case: Building a list component that needs to display list items differently based on the context, such as a task list where each task type (e.g., meeting, deadline, reminder) has a distinct layout.
WhyTemplateRef
? A TemplateRef
allows the list component to accept a custom item template for rendering each list item. This method allows for the development of a highly reusable list component that can dynamically adjust its content presentation. It can handle various types of data with complex display logic.
3. Tab Components with Lazy-Loaded Content
Use Case: A tab component where each tab's content is not only different but also might be expensive to initialize (e.g., complex forms, charts, or external data).
WhyTemplateRef
? Using TemplateRef
inputs for each tab content allows the tab component to instantiate the content only when a tab is activated, potentially improving performance through lazy loading. This is more efficient than <ng-content>
, which would render all tabs' content upfront, regardless of whether it's currently visible or not.
4. Tooltip or Modal with Context-Specific Content
Use Case: Tooltips or modals that need to display information specific to the context where they are invoked, such as user details on a dashboard.
WhyTemplateRef
? By using a TemplateRef
to define the content of a tooltip or modal, you can ensure that the content is both customizable and dynamically loaded based on the specific context (e.g., the user being hovered over). This level of dynamic content injection and context sensitivity is difficult to achieve with static <ng-content>
where the content may change depending on user details.
5. Custom Form Field Wrappers
Use Case: Creating a form field wrapper component to standardize the layout, validation messages, and styles of form fields across an application.
WhyTemplateRef
? Accepting a TemplateRef
for the input field allows the wrapper component to dynamically render different types of inputs (text, select, date picker) with standardized label, error, and hint layouts. It gives you the flexibility to use any form control within the standardized wrapper, a scenario where <ng-content>
would be less efficient due to its inability to apply different templates or contexts dynamically.