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ì
- Ba cách gặp ng-template
- Render một template bằng NgTemplateOutlet
- Truyền template vào component tái sử dụng
- So sánh với @if, ng-container và ng-content
- TemplateRef nâng cao với ViewContainerRef
- Lỗi thường gặp
- Checklist chọn giải pháp
- Tóm tắt
- Đi tiếp
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:
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-container | ng-template | |
|---|---|---|
| Nội dung bên trong | Render theo flow/directive bao quanh | Chờ Angular tạo embedded view |
| Mục đích | Áp dụng logic chung cho nhiều node mà không thêm wrapper | Khai 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ò:
<ng-template #greeting>giữ khuôn có têngreeting.<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ền | Khai báo trong template | Giá trị local |
|---|---|---|
$implicit: selectedUser() | let-user | selectedUser() |
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, @switch | Không thêm wrapper riêng | Component viết block | Điều kiện, lặp, switch thông thường trong template hiện đại |
ng-container | Không | Directive/block bao quanh nó | Gom cấu trúc hoặc làm vị trí đặt outlet mà không thêm element |
ng-template | Không render cho đến khi được instantiate | Directive, outlet hoặc code gọi view container | Khai báo UI render trễ, render lại, hoặc render với context |
ng-content | Không phải wrapper | Consumer cung cấp node; component có slot cố định | Composition tĩnh như header, body, action của panel/modal |
Chọn theo nhu cầu:
- Với một nhánh UI ở cùng template, dùng
@ifhoặc@for. - Khi cần thêm cấp cấu trúc mà không thêm thẻ DOM, dùng
ng-container. - 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-templatecùngTemplateRef. - 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ứng | Nguyên nhân | Cách xử lý |
|---|---|---|
Nội dung trong ng-template không hiện | Không directive/outlet nào instantiate template | Dùng @if, NgTemplateOutlet hoặc API ViewContainerRef theo đúng nhu cầu. |
Can't bind to 'ngTemplateOutlet' | Standalone component chưa import NgTemplateOutlet | Thêm imports: [NgTemplateOutlet], hoặc import CommonModule nếu thực sự cần nhiều directive/pipe chung. |
let-user là undefined | Context thiếu $implicit hoặc key mapping sai | Truyề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 template | Template đọ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 input | Nếu component cần quyết định lúc render, nhận TemplateRef và dùng outlet. |
Dùng ng-template cho mọi else | Template name làm nhánh đơn giản bị cách xa nhau | Vớ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 element | Một element chỉ có một structural-directive microsyntax | Dù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,@forhoặc@switchkhông đủ đơn giản cho trường hợp này. - Standalone component dùng outlet đã import
NgTemplateOutlet. - Context là object nhỏ, dùng
$implicithoặ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-contentphù 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-templategiữ một khuôn Angular. Angular tạo embedded view khi directive, outlet hoặc view container yêu cầu.#templateNametrênng-templatetạo mộtTemplateRefđể truyền, lưu hoặc render qua outlet.NgTemplateOutletrenderTemplateReftại vị trí hiện tại.$implicitánh xạ vàolet-namekhô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-containercho cấu trúc không wrapper,ng-contentcho composition tĩnh, vàng-templatecho khuôn render linh hoạt.