Angular Learning
Component & Template

ng-template, TemplateRef và NgTemplateOutlet

Hiểu khi nào Angular tạo embedded view, cách truyền context vào template và chọn ng-template thay cho control flow hoặc ng-content.

Phạm vi bài này

<ng-template> là một khuôn UI. Angular chỉ tạo giao diện từ khuôn đó khi một directive, outlet hoặc đoạn code yêu cầu. Bài này giải thích TemplateRef, NgTemplateOutlet và context của template.

Mục lục

ng-template là gì

Hãy coi <ng-template> là một mẫu chưa dùng ngay. Angular biên dịch phần HTML bên trong thành một khuôn. Khi cần hiển thị, Angular tạo một embedded view, tức một nhóm node DOM có binding và lifecycle riêng.

Ví dụ này mới chỉ khai báo nội dung. Nó chưa hiện trên trang vì chưa có nơi nào yêu cầu Angular render template.

<ng-template>
  <p>Chào mừng bạn quay lại.</p>
</ng-template>

Luồng hoạt động có thể hình dung như sau:

direction: right

n0: "<ng-template>\nKhuôn chưa hiển thị" {
  width: 176
  height: 58
}

n1: "TemplateRef\nDirective /\nNgTemplateOutlet /\nViewContainerRef yêu\ncầu render" {
  width: 184
  height: 121
}

n2: "Embedded view\nTạo binding, chèn node" {
  width: 200
  height: 58
}

n3: "DOM đang hiển thị" {
  width: 160
  height: 58
}

n0 -> n1
n1 -> n2
n2 -> n3
<ng-template>Khuôn chưa hiển thịTemplateRefDirective /NgTemplateOutlet /ViewContainerRef yêucầu renderEmbedded viewTạo binding, chèn nodeDOM đang hiển thị
<ng-template>Khuôn chưa hiển thịTemplateRefDirective /NgTemplateOutlet /ViewContainerRef yêucầu renderEmbedded viewTạo binding, chèn nodeDOM đang hiển thị
TemplateRef chỉ tạo DOM khi được yêu cầu

Không phải thẻ bọc DOM

<ng-template> không hoạt động như một div ẩn bằng CSS. Khi Angular chưa tạo embedded view, các node bên trong template chưa có mặt trong DOM hiển thị.

Trong ví dụ sau, section chỉ tồn tại khi isLoggedIn là true:

<ng-template [ngIf]="isLoggedIn">
  <section class="account-panel">Nội dung tài khoản</section>
</ng-template>

Angular ng-template cũng khác native HTML <template>. Angular compiler theo dõi binding, dependency injection và lifecycle của template. Khi làm việc trong ứng dụng Angular, bạn sẽ thường dùng TemplateRef và NgTemplateOutlet.

ng-container khác ng-template thế nào

Cả ng-container và ng-template đều không tạo một thẻ HTML riêng trong DOM. Khác biệt nằm ở nội dung bên trong.

ng-container chỉ nhóm các node con để Angular áp dụng một directive hoặc control flow chung. Khi điều kiện đúng, Angular render ngay các node bên trong.

<ng-container *ngIf="isLoggedIn">
  <h2>Tài khoản</h2>
  <button type="button">Đăng xuất</button>
</ng-container>

Nếu isLoggedIn là true, DOM có h2 và button. Nếu là false, cả hai không được tạo. ng-container không còn lại như một wrapper trong DOM.

ng-template giữ một khuôn để render sau. Viết template thôi chưa làm nội dung xuất hiện:

<ng-template #emptyState>
  <p>Chưa có dữ liệu.</p>
</ng-template>

Để hiển thị nội dung đó, một directive hoặc code phải yêu cầu Angular tạo view, ví dụ:

<ng-container [ngTemplateOutlet]="emptyState" />
ng-containerng-template
Nội dung bên trongRender theo flow/directive bao quanhChờ Angular tạo embedded view
Mục đíchÁp dụng logic chung cho nhiều node mà không thêm wrapperKhai báo khuôn để render sau, render lại hoặc truyền vào component
Ví dụ thường gặp*ngIf, *ngFor, NgTemplateOutlet#emptyState, #itemTemplate, template input

Dùng ng-container khi bạn chỉ cần một vị trí nhóm node. Dùng ng-template khi bạn cần giữ lại một khuôn UI để nơi khác render.

TemplateRef và embedded view

Thêm biến tham chiếu vào ng-template sẽ cho bạn một TemplateRef.

<ng-template #emptyState>
  <p>Chưa có đơn hàng.</p>
</ng-template>

emptyState không trỏ đến một DOM element. Nó giữ khuôn mà Angular có thể dùng để tạo embedded view sau này.

Template vẫn giữ binding context và injection context của nơi nó được khai báo. Chẳng hạn, một template do trang sản phẩm khai báo vẫn đọc được state và method của trang sản phẩm, kể cả khi một component list là nơi render nó.

Embedded view là gì

Embedded view là phần giao diện Angular tạo từ một template tại thời điểm cần hiển thị. Nó gồm các node DOM thật, binding của chúng và các biến cục bộ mà template khai báo.

Ví dụ, khi isLoggedIn() là true, Angular tạo embedded view cho phần bên trong @if:

@if (isLoggedIn()) {
  <p>Xin chào {{ user().name }}</p>
}

Lúc đó DOM có một thẻ p với tên người dùng. Khi isLoggedIn() thành false, Angular hủy embedded view đó và thẻ p biến mất khỏi DOM.

Với ng-template, luồng này hiện rõ hơn:

<ng-template #emptyState>
  <p>Chưa có sản phẩm.</p>
</ng-template>

<ng-container [ngTemplateOutlet]="emptyState" />

emptyState giữ khuôn. NgTemplateOutlet yêu cầu Angular tạo embedded view từ khuôn ấy tại vị trí của ng-container. Kết quả hiển thị là thẻ p thật trong DOM.

Embedded view không tạo một component class mới. Nó là một phần view nhỏ mà Angular tạo và hủy cho các trường hợp như @if, @for, *ngIf, *ngFor, NgTemplateOutlet hoặc ViewContainerRef.createEmbeddedView().

Ba cách gặp ng-template

Cú pháp rút gọn của structural directive

Dấu * là cú pháp ngắn cho một ng-template bao quanh element đang dùng directive.

<p *ngIf="isLoggedIn">Xin chào {{ user.name }}</p>

Angular hiểu đoạn trên gần như sau:

<ng-template [ngIf]="isLoggedIn">
  <p>Xin chào {{ user.name }}</p>
</ng-template>

Một element chỉ đặt được một structural directive có dấu *. Vì *ngIf và *ngFor đều được Angular mở rộng thành một ng-template bao quanh element đó, Angular không thể đặt cả hai trên cùng một thẻ p.

Tách chúng thành hai cấp. ng-container chỉ giúp nhóm cấu trúc, nên không tạo thêm thẻ trong DOM:

<ng-container *ngIf="isLoggedIn">
  <p *ngFor="let notification of notifications">
    {{ notification.message }}
  </p>
</ng-container>

Angular kiểm tra isLoggedIn trước. Nếu giá trị là false, toàn bộ danh sách không được tạo. Nếu là true, Angular mới lặp notifications và tạo một thẻ p cho mỗi thông báo.

Mã Angular mới

Dùng @if, @for và @switch cho điều kiện, vòng lặp và rẽ nhánh thông thường. ng-template vẫn cần thiết khi làm việc với structural directive cũ, NgTemplateOutlet, template input hoặc thư viện UI.

Template có tên

Mã Angular cũ thường dùng template có tên cho nhánh else của NgIf.

<div *ngIf="user; else loggedOut">
  Xin chào {{ user.name }}
</div>

<ng-template #loggedOut>
  <a routerLink="/login">Đăng nhập</a>
</ng-template>

loggedOut là template NgIf sẽ render khi user không có giá trị. Với một nhánh chỉ dùng ở đây, @if ... @else đặt hai trường hợp cạnh nhau và dễ đọc hơn:

@if (user) {
  <div>Xin chào {{ user.name }}</div>
} @else {
  <a routerLink="/login">Đăng nhập</a>
}

Giữ ng-template khi bạn cần render cùng một khuôn ở nhiều vị trí, truyền khuôn vào component, hoặc truyền context khác nhau mỗi lần render.

Template được render qua outlet

NgTemplateOutlet nhận một TemplateRef rồi tạo embedded view tại vị trí của outlet. Đây là lựa chọn phù hợp khi bạn viết UI một lần và để component quyết định vị trí hoặc số lần hiển thị.

Ví dụ này đặt hai khuôn ở cuối template. Vùng section chọn một trong hai khuôn để render.

<section class="results">
  <ng-container [ngTemplateOutlet]="results().length ? resultList : emptyState" />
</section>

<ng-template #resultList>
  <ul>
    @for (result of results(); track result.id) {
      <li>{{ result.name }}</li>
    }
  </ul>
</ng-template>

<ng-template #emptyState>
  <p>Không có kết quả phù hợp.</p>
</ng-template>

ng-container là điểm Angular chèn view vào và không tạo thêm element DOM. resultList hoặc emptyState chỉ tạo node DOM khi outlet chọn chúng.

Render một template bằng NgTemplateOutlet

NgTemplateOutlet có một việc: lấy một template đã đặt tên và hiển thị nó tại vị trí của outlet.

Hãy đọc ví dụ theo hai vai trò:

  1. <ng-template #greeting> giữ khuôn có tên greeting.
  2. <ng-container [ngTemplateOutlet]="greeting" /> là vị trí yêu cầu Angular hiển thị khuôn đó.

Ví dụ nhỏ nhất

<ng-template #greeting>
  <p>Xin chào.</p>
</ng-template>

<ng-container [ngTemplateOutlet]="greeting" />

Angular không hiển thị thẻ ng-template. Nó hiển thị phần bên trong template tại chỗ của ng-container. DOM kết quả có một thẻ p:

<p>Xin chào.</p>

ng-container là một vị trí chèn nội dung và không tạo wrapper trong DOM. Bạn có thể đặt outlet ở bất kỳ chỗ nào muốn nội dung xuất hiện.

Import cần thiết cho standalone component

NgTemplateOutlet là directive của @angular/common. Standalone component phải khai báo directive này trong imports.

import { Component } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

@Component({
  selector: 'app-order-page',
  standalone: true,
  imports: [NgTemplateOutlet],
  templateUrl: './order-page.html',
})
export class OrderPageComponent {}

CommonModule cũng cung cấp NgTemplateOutlet. Import trực tiếp thường dễ theo dõi hơn khi component chỉ dùng directive này.

Context và cú pháp let

Template thường cần dữ liệu. Dùng [ngTemplateOutletContext] để đưa dữ liệu vào template. Bắt đầu với một giá trị qua key $implicit:

<ng-template #userCard let-user>
  <p>{{ user.name }}</p>
</ng-template>

<ng-container
  [ngTemplateOutlet]="userCard"
  [ngTemplateOutletContext]="{ $implicit: selectedUser() }"
/>

selectedUser() được đưa vào $implicit, nên let-user nhận giá trị đó. Có thể hiểu let-user là biến cục bộ chỉ dùng bên trong userCard.

Khi template cần nhiều giá trị, thêm các key có tên vào context:

<ng-container
  [ngTemplateOutlet]="userCard"
  [ngTemplateOutletContext]="{
    $implicit: selectedUser(),
    index: selectedIndex(),
    canEdit: canEdit()
  }"
/>

<ng-template #userCard let-user let-position="index" let-editable="canEdit">
  <article>
    <h2>{{ position + 1 }}. {{ user.name }}</h2>
    @if (editable) {
      <button type="button">Chỉnh sửa</button>
    }
  </article>
</ng-template>

Bảng sau cho thấy từng local variable nhận giá trị nào:

Context được truyềnKhai báo trong templateGiá trị local
$implicit: selectedUser()let-userselectedUser()
index: selectedIndex()let-position="index"selectedIndex()
canEdit: canEdit()let-editable="canEdit"canEdit()

let-user không chỉ rõ key nên Angular lấy $implicit. let-position="index" lấy key index. Tên local variable do người viết template đặt, vì vậy nó không cần trùng với key trong context.

Context phải là object

[ngTemplateOutletContext]="selectedUser()" không tạo local variable theo cách outlet mong đợi. Dùng { $implicit: selectedUser() } nếu template cần nhận một giá trị chính. Object context cũng cho thấy rõ template được phép dùng dữ liệu nào.

Khai báo kiểu cho context

Khi truyền template qua input, hãy tạo một type cho context. Type này là hợp đồng giữa component render template và nơi cung cấp template.

import { Component, TemplateRef, input } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';

type Product = {
  id: number;
  name: string;
  price: number;
};

type ProductItemContext = {
  $implicit: Product;
  position: number;
  selected: boolean;
};

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [NgTemplateOutlet],
  template: `
    <ul>
      @for (product of products(); track product.id; let position = $index) {
        <li>
          <ng-container
            [ngTemplateOutlet]="itemTemplate()"
            [ngTemplateOutletContext]="{
              $implicit: product,
              position,
              selected: product.id === selectedId()
            }"
          />
        </li>
      }
    </ul>
  `,
})
export class ProductListComponent {
  readonly products = input.required<Product[]>();
  readonly selectedId = input<number | null>(null);
  readonly itemTemplate = input.required<TemplateRef<ProductItemContext>>();
}

TemplateRef<ProductItemContext> cho component biết phải cấp các key nào. Khả năng kiểm tra kiểu bên trong template còn phụ thuộc vào cấu hình Angular và context guard mà directive hoặc component công bố. Dù vậy, context vẫn nên nhỏ và dùng tên dễ hiểu.

Truyền template vào component tái sử dụng

Một list hoặc data table thường biết cách lặp dữ liệu, nhưng không nên ép mọi màn hình dùng cùng markup cho một item. Template input cho phép consumer đưa markup vào, còn list vẫn quản lý vị trí render và số lần lặp.

Consumer khai báo template trong context của chính nó:

<ng-template #productItem let-product let-position="position" let-selected="selected">
  <button
    type="button"
    [class.is-selected]="selected"
    (click)="selectProduct(product.id)"
  >
    {{ position + 1 }}. {{ product.name }} — {{ product.price | number }} đ
  </button>
</ng-template>

<app-product-list
  [products]="products()"
  [selectedId]="selectedProductId()"
  [itemTemplate]="productItem"
/>

productItem vẫn gọi được selectProduct() và đọc selectedProductId() của consumer vì template được khai báo ở đó. ProductListComponent chỉ cấp product, position và selected qua context.

Phân chia trách nhiệm trong ví dụ này:

  • Component list quản lý vòng lặp, track, trạng thái loading/empty và vị trí render.
  • Consumer quyết định markup của item và xử lý event thuộc màn hình.
  • Context là API công khai của list. Việc đổi tên hoặc bỏ một key có thể làm template consumer hỏng.

So sánh với @if, ng-container và ng-content

Các cú pháp này xuất hiện trong template Angular, nhưng mỗi cái phục vụ một việc riêng.

Công cụCó tạo DOM wrapper?Ai quyết định khi nào render?Trường hợp nên dùng
@if, @for, @switchKhông thêm wrapper riêngComponent viết blockĐiều kiện, lặp, switch thông thường trong template hiện đại
ng-containerKhôngDirective/block bao quanh nóGom cấu trúc hoặc làm vị trí đặt outlet mà không thêm element
ng-templateKhông render cho đến khi được instantiateDirective, outlet hoặc code gọi view containerKhai báo UI render trễ, render lại, hoặc render với context
ng-contentKhông phải wrapperConsumer cung cấp node; component có slot cố địnhComposition tĩnh như header, body, action của panel/modal

Chọn theo nhu cầu:

  1. Với một nhánh UI ở cùng template, dùng @if hoặc @for.
  2. Khi cần thêm cấp cấu trúc mà không thêm thẻ DOM, dùng ng-container.
  3. Khi component cần render một khuôn ở vị trí khác, nhiều lần, hoặc với context riêng, dùng ng-template cùng TemplateRef.
  4. Khi consumer chỉ đặt nội dung một lần vào slot cố định, dùng ng-content.

Không dùng ng-template làm wrapper vô hình

Chọn ng-container nếu bạn chỉ muốn tránh thêm một div. ng-template thay đổi thời điểm Angular tạo view, nên cần một directive, outlet hoặc view container để render nội dung.

TemplateRef nâng cao với ViewContainerRef

NgTemplateOutlet đáp ứng phần lớn trường hợp khai báo trong HTML. Khi viết directive hoặc component hạ tầng, bạn có thể inject TemplateRef và ViewContainerRef để tự tạo embedded view.

Directive dưới đây chỉ render nội dung khi condition là true:

import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appIfAllowed]',
  standalone: true,
})
export class IfAllowedDirective {
  constructor(
    private readonly templateRef: TemplateRef<unknown>,
    private readonly viewContainer: ViewContainerRef,
  ) {}

  @Input()
  set appIfAllowed(allowed: boolean) {
    this.viewContainer.clear();

    if (allowed) {
      this.viewContainer.createEmbeddedView(this.templateRef);
    }
  }
}

Dùng directive bằng microsyntax:

<button *appIfAllowed="canDelete()" type="button">Xóa</button>

Angular cấp TemplateRef của phần được dấu * mở rộng và ViewContainerRef là vị trí chèn view. Directive dùng trong production nên tránh xóa và tạo view không cần thiết, có thể giữ lại EmbeddedViewRef, và cần test khi condition đổi liên tục. Khi chỉ cần điều kiện trong một component, @if vẫn đơn giản hơn.

Lỗi thường gặp

Triệu chứngNguyên nhânCách xử lý
Nội dung trong ng-template không hiệnKhông directive/outlet nào instantiate templateDùng @if, NgTemplateOutlet hoặc API ViewContainerRef theo đúng nhu cầu.
Can't bind to 'ngTemplateOutlet'Standalone component chưa import NgTemplateOutletThêm imports: [NgTemplateOutlet], hoặc import CommonModule nếu thực sự cần nhiều directive/pipe chung.
let-user là undefinedContext thiếu $implicit hoặc key mapping saiTruyền { $implicit: user }, hoặc dùng let-user="keyName" đúng với key context.
Template được render nhưng đọc state “sai nơi”Nhầm declaration context với nơi outlet đặt templateTemplate đọc binding/injector từ nơi nó được khai báo. Hãy cung cấp dữ liệu thay đổi theo outlet qua context rõ ràng.
Dùng ng-content bên trong ng-template để mong chiếu nội dung trễProjection có semantics khác lazy template; projected content không phải template inputNếu component cần quyết định lúc render, nhận TemplateRef và dùng outlet.
Dùng ng-template cho mọi elseTemplate name làm nhánh đơn giản bị cách xa nhauVới code mới, ưu tiên @if ... @else nếu nhánh chỉ dùng một lần.
Cố dùng hai * trên cùng elementMột element chỉ có một structural-directive microsyntaxDùng ng-container, hoặc chuyển logic sang built-in control flow.

Checklist chọn giải pháp

Trước khi thêm ng-template, kiểm tra:

  • UI cần render trễ, render nhiều lần, render ở vị trí khác hoặc nhận context riêng.
  • @if, @for hoặc @switch không đủ đơn giản cho trường hợp này.
  • Standalone component dùng outlet đã import NgTemplateOutlet.
  • Context là object nhỏ, dùng $implicit hoặc key có tên rõ ràng.
  • TemplateRef<Context> mô tả context khi template đi qua input hoặc API công khai.
  • Consumer biết template có thể được gọi bao nhiêu lần và nhận những key nào.
  • ng-content phù hợp hơn nếu nội dung chỉ project một lần vào slot cố định.
  • Bạn đã test empty state, context thiếu dữ liệu và event trong template consumer.

Tóm tắt

  • ng-template giữ một khuôn Angular. Angular tạo embedded view khi directive, outlet hoặc view container yêu cầu.
  • #templateName trên ng-template tạo một TemplateRef để truyền, lưu hoặc render qua outlet.
  • NgTemplateOutlet render TemplateRef tại vị trí hiện tại. $implicit ánh xạ vào let-name không có key.
  • Template giữ binding context từ nơi khai báo. Component render template nên truyền dữ liệu của mình qua context.
  • Dùng control flow cho điều kiện và vòng lặp, ng-container cho cấu trúc không wrapper, ng-content cho composition tĩnh, và ng-template cho khuôn render linh hoạt.

Đi tiếp

On this page