When Angular uses third-party UI libraries, it is common to pass custom templates to third-party components via ng-template to meet business requirements. The Tabs component of NGX-Bootstrap, for example, allows you to customize the TAB template.
<div>
<tabset>
<tab>
<ng-template tabHeading>
<i><b>Tab 3</b></i>
</ng-template>
Tab with html tags in heading
</tab>
</tabset>
</div>
Copy the code
Today’s article explores the rationale behind this and implements a component with similar functionality step by step.
Counter component
Start by creating a basic Counter component. This component displays the current value and can increment and decrement. In addition, you can customize the initial value and emit an event when the value changes.
The initial code is as follows:
import {
Component,
ChangeDetectionStrategy,
Input,
Output,
EventEmitter,
} from '@angular/core';
@Component({
selector: 'app-counter'.templateUrl: './counter.component.html'.styleUrls: ['./counter.component.scss'].changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CounterComponent {
@Input() value = 0;
@Output() changed = new EventEmitter<number> ();increment() {
this.updateValue('inc');
}
decrement() {
this.updateValue('dec');
}
private updateValue(action: 'inc' | 'dec') {
const delta = action === 'inc' ? 1 : -1;
this.value += delta;
this.changed.emit(this.value); }}Copy the code
The HTML content is as follows:
<h2>Counter</h2>
<p class="mb-3">{{ value }}</p>
<div class="btns">
<button class="mr-3" (click) ="increment()">inc</button>
<button (click) ="decrement()">dec</button>
</div>
Copy the code
Configurable template
In Angular, view templates can be created with the ng-template element. Once a template is generated, it can be passed to a reusable component using Content projection, and the component will be used as follows:
<app-custom-counter title="Fancy counter">
<ng-template appCounterValue let-value>
<span class="mr-2">Current value: {{value}}</span>
<i class="fa fa-arrow-left"></i>
</ng-template>
<ng-template appCounterIncBtn let-increment>
<button class="btn btn-success" (click) ="increment()">
inc <i class="fa fa-arrow-up"></i>
</button>
</ng-template>
<ng-template appCounterDecBtn let-decrement>
<button class="btn btn-danger" (click) ="decrement()">
dec <i class="fa fa-arrow-down"></i>
</button>
</ng-template>
</app-custom-counter>
Copy the code
As you can see from the above writing, each view template has its own directives, so we can tell which part of the component each ng-template belongs to.
Let’s start by implementing the three directives above, each of which is simple: you only need to expose a reference to the view template.
export interface CounterValueTplContext {
$implicit: number;
}
export interface CounterBtnTplContext {
$implicit: () = > void;
}
Copy the code
import { Directive, TemplateRef } from '@angular/core';
import { CounterValueTplContext } from './custom-counter.component';
@Directive({
selector: '[appCounterValue]',})export class CounterValueDirective {
constructor(readonly tpl: TemplateRef<CounterValueTplContext>){}}Copy the code
import { Directive, TemplateRef } from '@angular/core';
import { CounterBtnTplContext } from './custom-counter.component';
@Directive({
selector: '[appCounterIncBtn]',})export class CounterIncBtnDirective {
constructor(readonly tpl: TemplateRef<CounterBtnTplContext>){}}Copy the code
import { Directive, TemplateRef } from '@angular/core';
import { CounterBtnTplContext } from './custom-counter.component';
@Directive({
selector: '[appCounterDecBtn]',})export class CounterDecBtnDirective {
constructor(readonly tpl: TemplateRef<CounterBtnTplContext>){}}Copy the code
Next, we can use the ContentChild decorator in the component to get a reference to the view template. The first argument is the class name of the directive:
import {
Component,
ChangeDetectionStrategy,
Input,
Output,
EventEmitter,
ContentChild,
TemplateRef,
} from '@angular/core';
import { CounterValueDirective } from './counter-value.directive';
import { CounterIncBtnDirective } from './counter-inc-btn.directive';
import { CounterDecBtnDirective } from './counter-dec-btn.directive';
@Component({
selector: 'app-custom-counter'.templateUrl: './custom-counter.component.html'.styleUrls: ['./custom-counter.component.css'].changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomCounterComponent {
@ContentChild(CounterValueDirective, { static: true}) counterValueDir! : CounterValueDirective;@ContentChild(CounterIncBtnDirective, { static: true}) counterIncBtnDir! : CounterIncBtnDirective;@ContentChild(CounterDecBtnDirective, { static: true}) counterDecBtnDir! : CounterDecBtnDirective;@Input() title = 'Counter';
@Input() value = 0;
@Output() changed = new EventEmitter<number> ();get counterValueTpl() :TemplateRef<CounterValueTplContext> {
return this.counterValueDir? .tpl; }get counterIncBtnTpl() :TemplateRef<CounterBtnTplContext> {
return this.counterIncBtnDir? .tpl; }get counterDecBtnTpl() :TemplateRef<CounterBtnTplContext> {
return this.counterDecBtnDir? .tpl; }get counterValueTplContext() :CounterValueTplContext {
return { $implicit: this.value };
}
get counterIncBtnTplContext() :CounterBtnTplContext {
return { $implicit: () = > this.increment() };
}
get counterDecBtnTplContext() :CounterBtnTplContext {
return { $implicit: () = > this.decrement() };
}
increment() {
this.updateValue('inc');
}
decrement() {
this.updateValue('dec');
}
private updateValue(action: 'inc' | 'dec') {
const delta = action === 'inc' ? 1 : -1;
this.value += delta;
this.changed.emit(this.value); }}Copy the code
In addition to getting getters for the template, there are getters for context objects passed to the view template to pass data to the ng-template.
Finally, in the HTML template, we render the view template in the appropriate position of the component using the NgTemplateOutlet directive. In most third-party UI libraries, if a custom template is not passed, the default template is used.
<h2>{{title}}</h2>
<div class="mb-3">
<ng-container *ngTemplateOutlet="counterValueTpl || defaultValueTpl; context:counterValueTplContext">
</ng-container>
</div>
<div class="d-flex justify-content-center">
<div class="mr-3">
<ng-container *ngTemplateOutlet="counterIncBtnTpl || defaultIncBtnTpl; context:counterIncBtnTplContext">
</ng-container>
</div>
<div>
<ng-container *ngTemplateOutlet="counterDecBtnTpl || defaultDecBtnTpl; context:counterDecBtnTplContext">
</ng-container>
</div>
</div>
<ng-template #defaultValueTpl>
{{value}}
</ng-template>
<ng-template #defaultIncBtnTpl>
<button (click) ="increment()">inc</button>
</ng-template>
<ng-template #defaultDecBtnTpl>
<button (click) ="decrement()">dec</button>
</ng-template>
Copy the code
At this point, we have basically implemented a reusable Angular component that can customize templates.
extension
ng-template
In the above implementation, the context passed to ng-template returns an object:
get counterValueTplContext() :CounterValueTplContext {
return { $implicit: this.value };
}
Copy the code
The $implicit property of this object is the variable corresponding to let-value. We can also pass multiple objects, such as;
get counterValueTplContext() :CounterValueTplContext {
return { $implicit: this.value, unit: 'million' };
}
Copy the code
<ng-template appCounterValue let-value let-unit='unit'>
<span class="mr-2">Current value: {{ value }} {{ unit }}</span>
<i class="fa fa-arrow-left"></i>
</ng-template>
Copy the code
Where let-value is not assigned, it is automatically assigned a value of $implicit. Unit corresponds to the Unit property in the returned context object.
Use the * Syntax
If you need to pass only one value to ng-template, you can use a simpler syntax:
<ng-container *appCounterValue="let value">
<span class="mr-2">Current value: {{value}}</span>
<i class="fa fa-arrow-left"></i>
</ng-container>
Copy the code
string token
When passing the first argument to the ContentChild decorator, you can also pass a string that must be a unique template reference variable.
// counter component class
@ContentChild('counterValue', { static: true, read: TemplateRef }) counterValueTpl: TemplateRef<any>;
// parent component view
<app-custom-counter title="Fancy counter">
<ng-template #counterValue let-value>
<div>
<span class="mr-2">Current value: {{value}}</span>
<i class="fa fa-arrow-left"></i>
</div>
</ng-template>.</app-custom-counter>
Copy the code
conclusion
In Angular, there are multiple solutions for different requirements scenarios. This is appropriate if we want to reuse the layout of components. If you just want to pass components through content projection, then using component lazy instantiation might be a better approach. In actual development, you need to choose the appropriate implementation for different scenarios.
Reference links:
Reusable components with configurable templates in Angular
How to use ng-template & TemplateRef in Angular
What is let-* in Angular 2 templates?