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?