Nhảy đến nội dung

Sự khác biệt giữa ng-container và ng-template trong Angular

Nội Dung Bài Viết

ng-containerng-template là các phần tử Angular hoạt động giống như các thùng chứa với các phần tử HTML, thông qua các thành phần này Angular có thể render code HTML theo những quy tắc riêng của nó. Do đó, chúng có những mục đích sử dụng cho các trường hợp khác nhau. Để hiểu hơn chúng ta hãy lướt qua từng phần.

ng-container là gì

Nếu bạn đã quen thuộc với React, bạn có thể biết thành phần Fragment React. Thành phần này được sử dụng khi bạn không muốn thêm một phần tử HTML bổ sung vào DOM (như div hoặc span), nhưng bạn muốn có một trình bao bọc xung quanh các thành phần con.

ng-container hoạt động giống như vậy và nó cũng chấp nhận các chỉ thị cấu trúc (Structure Directive)  Angular (ngIf, ngFor, ...) . Chúng là các phần tử có thể đóng vai trò là trình bao bọc nhưng không thêm phần tử bổ sung vào DOM.

Hãy xem một ví dụ mà bạn có thể muốn sử dụng cái này:

// app.component.ts
export class AppComponent {
  products = [
    {
      id: "iphone",
      name: "iPhone",
      price: "599",
    },
    {
      id: "samsung",
      name: "Samsung",
      price: "699",
    },
    {
      name: "Pixel",
      price: "599",
    },
  ]
}
<!-- app.component.html -->
<div *ngFor="let product of products">
  <span>{{ product.name }}</span>
</div>

Ở ví dụ này, trong HTML, chúng ta có thể lặp qua mảng sản phẩm và hiển thị tên sản phẩm trong thẻ span.

Nhưng điều gì sẽ xảy ra nếu bạn muốn hiển thị thẻ div một cách có điều kiện; nghĩa là, nếu sản phẩm có thuộc tính id? thì hiển thị. Sau đó, chúng ta có thể sử dụng Structure Directive * ngIf, có thể như thế này:

<div *ngFor="let product of products" *ngIf="product.id">
  <span>{{ product.name }}</span>
</div>

Nhưng, điều này sẽ tạo ra một lỗi.

Như bạn có thể đã biết, trong Angular, bạn không thể áp dụng hai hoặc nhiều chỉ Structure Directive trên một phần tử. Giải pháp tiếp theo của bạn có thể là:

<div *ngFor="let product of products">
  <div *ngIf="product.id">
    <span>{{ product.name }}</span>
  </div>
</div>

Mã này sẽ hoạt động, nhưng bây giờ bạn đang thấy một phần tử div mà bạn có thể không cần.

Chúng ta có thể giải quyết vấn đề này với ng-container như sau:

<ng-container *ngFor="let product of products">
  <div *ngIf="product.id">
    <span>{{ product.name }}</span>
  </div>
</ng-container>

Với điều này, DOM sẽ không bao gồm ng-container, vì vậy chúng ta không thêm một phần tử bổ sung mà không cần. Angular sẽ hiển thị phần tử vùng chứa dưới dạng nhận xét trong DOM. Và nếu bạn cần sử dụng một chỉ thị cấu trúc khác trước phần tử div, bạn có thể sử dụng thêm ng-container.

ng-template là gì

Hãy hiểu về ng-template như một template chứa các phần tử template ví dụ như đoạn code HTML, nhưng Angular sẽ không hiển thị nó theo mặc định. Nó chỉ thực hiện khi bạn chỉ định cho phép nó sẽ được hiển thị.

Xem ví dụ duới

<h1>Hello</h1>

<ng-template>
  <h2>How are you</h2>
</ng-template>

Trong mã nguồn HMTL trong trình duyệt, bạn sẽ thấy phần thứ hai được nhận xét như thế này:

<h1 _ngcontent-agr-c11>Hello</h1>

<!-- -->

ng-template không được kết xuất và nó chỉ được định nghĩa trong mã nguồn.

Hãy xem xét một trường hợp sử dụng cho phần tử này:

<ng-template #noProducts>
  <p>There are no products in this store</p>
</ng-template>

Ở trên, chúng tôi đã xác định một template với tên noProducts. Trong ví dụ này, chúng tôi sẽ kết xuất nó vào DOM nếu không có sản phẩm nào, như thế này:

<ng-container *ngIf="products.length > 0; else noProducts">
  <ng-container *ngFor="let product of products">
    <div *ngIf="product.id">
      <span>{{ product.name }}</span>
    </div>
  </ng-container>
</ng-container>

<ng-template #noProducts>
  <p>There are no products in this store</p>
</ng-template>

ng-template sẽ không bao giờ được hiển thị cho đến khi nó đủ điều kiện để hiển thị.

Chúng tôi có thể lưu trữ một đoạn mã giao diện người dùng với một phần tử trong template và áp dụng nó vào DOM một cách có điều kiện.

Khi nào thì nên dùng ng-template?

Theo bài viết 100-days-of-angular, là kinh nghiệm của các developer chia sẽ: 

1. Dùng kết hợp với các Structure Directive của Angular, ví dụ như *ngIf

<ng-container *ngIf="condition; else templateA">
  …
</ng-container>
<ng-template #templateA>
  …
</ng-template>

2. Khi một số UI element trong một component bị lặp lại trong chính component đó, nhưng phần code đó quá nhỏ để tách ra làm một component riêng.

Ví dụ như bạn có một component có chứa biến một biển tên là counter. Phần UI của counter này sẽ đc lặp lại ở trong component của bạn vài lần với UI giống nhau.

Đây là cách bình thường chúng ta hay làm. Copy and paste code.

<div class="card">
  <div class="card-header">
    You have selected
    <span class="badge badge-primary">{{ counter }}</span> items.
  </div>
  <div class="card-body">
    There are <span class="badge badge-primary">{{ counter }}</span> items was
    selected.
  </div>
  <div class="card-footer">
    You have selected
    <span class="badge badge-primary">{{ counter }}</span> items.
  </div>
</div>

Và đây là cách các developer viết lại bằng cách dùng ng-template và ngTemplateOutlet.

<div class="card">
  <div class="card-header">
    You have selected
    <ng-container [ngTemplateOutlet]="counterTmpl"></ng-container>.
  </div>
  <div class="card-body">
    There are <ng-container [ngTemplateOutlet]="counterTmpl"></ng-container> was
    selected.
  </div>
  <div class="card-footer">
    You have selected
    <ng-container [ngTemplateOutlet]="counterTmpl"></ng-container>.
  </div>
</div>

<ng-template #counterTmpl>
  <span class="badge badge-primary">{{ counter }}</span> items
</ng-template>

Khi viết lại code dùng ng-template, ưu điểm dễ nhận thấy đó là:

  • Nếu cần sửa lại UI cho counter. Thay vì phải sửa ở 3 nơi, bây giờ ta chỉ cần sửa ở một vị trí đó là ng-template của counter thôi. Tránh những lỗi typo hay find and replace bị thiếu.
  • Vì phần template này chỉ gói gọn trong đúng một dòng code nên dùng ng-template tiện hơn hẳn là phải tách phần counter ra một component mới.

3. Dùng ng-template để pass vào component khác. Hỗ trợ override template có sẵn trong component.

Ví dụ mình có component tab-container, mặc định sẽ render tab với template default là defaultTabButtonsTmpl.

@Component({
  selector: 'tab-container',
  template: `
    <ng-template #defaultTabButtonsTmpl>
      <div class="default-tab-buttons">...</div>
    </ng-template>
    <ng-container
      *ngTemplateOutlet="headerTemplate || defaultTabButtons"
    ></ng-container>
    ... rest of tab container component ...
  `,
})
export class TabContainerComponent {
  @Input() headerTemplate: TemplateRef<any>; // Custom template provided by parent
}

Tuy nhiên, khi dùng tab-container bạn hoàn toàn có thể pass vào template mới để override lại default UI ở parent component.

@Component({
  selector: 'app-root',
  template: `
    <ng-template #customTabButtons>
      <div class="custom-class">
        <button class="tab-button" (click)="login()">
          {{loginText}}
        </button>
        <button class="tab-button" (click)="signUp()">
          {{signUpText}}
        </button>
      </div>
    </ng-template>
    <tab-container [headerTemplate]="customTabButtons"></tab-container>
  `
})

Sự khác biệt giữa ng-container và ng-template

ng-container phục vụ như một vùng chứa cho các phần tử cũng có thể chấp nhận các chỉ thị cấu trúc (Structure Directive) nhưng không được hiển thị cho DOM, trong khi ng-template cho phép bạn tạo template có chứa các phần tử không được hiển thị cho đến khi thoả một điều kiện cụ thể để thêm nó vào DOM. .

Nguồn tham khảo: 

https://dillionmegida.com

https://github.com/angular-vietnam/100-days-of-angular/

https://angular.io/