Cú Pháp Control Flow trong Angular 2026: @if, @for, @switch và Câu Hỏi Phỏng Vấn

Tìm hiểu cú pháp control flow Angular (@if, @for, @switch) thay thế các structural directive. Hướng dẫn đầy đủ với ví dụ code, tips migration và câu hỏi phỏng vấn kỹ thuật.

Hướng dẫn cú pháp control flow Angular @if @for @switch

Cú pháp control flow của Angular (@if, @for, @switch) thay thế các structural directive *ngIf, *ngFor, và *ngSwitch đã thống trị template Angular trong nhiều năm. Được giới thiệu trong Angular 17, ổn định từ Angular 18, và trở thành mặc định trong Angular 22, cú pháp dựa trên block này đưa template gần hơn với các cấu trúc lập trình tiêu chuẩn đồng thời cho phép tối ưu hóa hiệu năng tốt hơn.

Migration có sẵn

Chạy ng generate @angular/core:control-flow để tự động migrate từ structural directive sang cú pháp control flow mới. Cả hai cú pháp có thể tồn tại song song trong quá trình migration, nhưng ngIf, ngFor, và ngSwitch đã deprecated từ Angular 19.

Tại Sao Angular Thay Thế Structural Directive Bằng Cú Pháp Block

Structural directive yêu cầu import CommonModule, sử dụng microsyntax khác với JavaScript, và buộc developer phải bọc nội dung trong ng-template cho các điều kiện phức tạp. Cú pháp control flow mới giải quyết những vấn đề này:

  • Không cần import: @if, @for, và @switch được tích hợp sẵn trong framework
  • Cú pháp giống JavaScript: điều kiện và vòng lặp đọc như code tiêu chuẩn
  • Hỗ trợ native @else: không cần tham chiếu ng-template cho nhánh else
  • Tree-shaking tốt hơn: các block control flow không sử dụng không ảnh hưởng đến kích thước bundle

Tài liệu Angular khuyến nghị áp dụng hoàn toàn cú pháp block trong tất cả dự án mới, và migrate codebase hiện có dần dần.

@if: Conditional Rendering Không Cần ng-template

Block @if render nội dung có điều kiện dựa trên biểu thức truthy. Khác với *ngIf, nó hỗ trợ trực tiếp các nhánh @else if@else:

user-status.component.tstypescript
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-user-status',
  template: `
    @if (user().role === 'admin') {
      <app-admin-panel />
    } @else if (user().role === 'editor') {
      <app-editor-panel />
    } @else {
      <app-viewer-panel />
    }
  `
})
export class UserStatusComponent {
  user = input.required<{ role: string }>();
}

Block @if cũng hỗ trợ aliasing biến với keyword as, trích xuất giá trị từ các biểu thức lồng nhau:

html
<!-- Tránh truy cập property lặp lại -->
@if (user().profile?.settings?.theme; as theme) {
  <p>Theme hiện tại: {{ theme }}</p>
}

Pattern này đặc biệt hữu ích khi làm việc với dữ liệu async hoặc object lồng sâu, vì biến được alias chỉ được định nghĩa khi điều kiện là truthy.

@for: Iteration Với Track Expression Bắt Buộc

Block @for iterate qua bất kỳ iterable JavaScript nào, với tối ưu hóa cho array. Khác với *ngFor, nó yêu cầu biểu thức track:

product-list.component.tstypescript
import { Component, input } from '@angular/core';

interface Product {
  id: string;
  name: string;
  price: number;
}

@Component({
  selector: 'app-product-list',
  template: `
    @for (product of products(); track product.id) {
      <app-product-card [product]="product" />
    } @empty {
      <p>Không có sản phẩm nào</p>
    }
  `
})
export class ProductListComponent {
  products = input.required<Product[]>();
}

Biểu thức track cho Angular biết cách xác định từng item qua các lần re-render. Chọn property tracking đúng ảnh hưởng trực tiếp đến hiệu năng:

Biểu Thức TrackTrường Hợp Sử DụngẢnh Hưởng Hiệu Năng
track item.idItem có identifier duy nhấtTối ưu: cập nhật DOM tối thiểu
track $indexDanh sách tĩnh không bao giờ sắp xếp lạiChấp nhận được: re-render hoàn toàn khi sắp xếp lại
track itemTracking tham chiếuKém: tham chiếu mới = node DOM mới

Biến Ngữ Cảnh Trong Block @for

Angular cung cấp các biến implicit bên trong block @for để expose metadata iteration:

html
@for (item of items(); track item.id; let i = $index, isLast = $last) {
  <li class="item" [class.last]="isLast">
    {{ i + 1 }}. {{ item.name }}
  </li>
}

Các biến ngữ cảnh có sẵn:

  • $index: vị trí bắt đầu từ 0
  • $count: tổng số item
  • $first, $last: flag boolean cho item đầu/cuối
  • $even, $odd: flag boolean dựa trên tính chẵn lẻ của index

Sẵn sàng chinh phục phỏng vấn Angular?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

@switch: Conditional Branching Type-Safe

Block @switch cung cấp pattern matching exhaustive với so sánh strict equality (===):

status-badge.component.tstypescript
import { Component, input } from '@angular/core';

type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered';

@Component({
  selector: 'app-status-badge',
  template: `
    @switch (status()) {
      @case ('pending') {
        <span class="badge badge-gray">Đang chờ</span>
      }
      @case ('processing') {
        <span class="badge badge-blue">Đang xử lý</span>
      }
      @case ('shipped') {
        <span class="badge badge-yellow">Đã gửi</span>
      }
      @case ('delivered') {
        <span class="badge badge-green">Đã giao</span>
      }
    }
  `
})
export class StatusBadgeComponent {
  status = input.required<OrderStatus>();
}

Khác với switch của JavaScript, @switch của Angular không có hành vi fallthrough. Nhiều điều kiện nhắm vào cùng block yêu cầu các câu lệnh @case liên tiếp:

html
@switch (userRole()) {
  @case ('admin')
  @case ('superadmin') {
    <app-admin-dashboard />
  }
  @case ('editor')
  @case ('reviewer') {
    <app-editor-dashboard />
  }
  @default {
    <app-viewer-dashboard />
  }
}

Kiểm Tra Type Exhaustive Với @default never

Angular 22 hỗ trợ kiểm tra exhaustiveness tại thời điểm compile. Sử dụng @default never; khai báo rằng không nên có case còn lại nào:

typescript
type Theme = 'light' | 'dark' | 'system';

// Lỗi compile nếu thiếu giá trị Theme từ các block @case
@switch (theme()) {
  @case ('light') { /* ... */ }
  @case ('dark') { /* ... */ }
  @case ('system') { /* ... */ }
  @default never;
}

Thêm giá trị mới vào union Theme sẽ trigger lỗi compile, buộc developer phải xử lý tất cả các case.

Cú Pháp Control Flow và Tích Hợp Angular Signals

Các block control flow tích hợp liền mạch với Angular Signals, cho phép reactivity fine-grained:

dashboard.component.tstypescript
import { Component, computed, signal } from '@angular/core';

@Component({
  selector: 'app-dashboard',
  template: `
    @if (isLoading()) {
      <app-skeleton />
    } @else if (hasError()) {
      <app-error [message]="errorMessage()" />
    } @else {
      @for (item of filteredItems(); track item.id) {
        <app-item-card [item]="item" />
      } @empty {
        <p>Không có item nào khớp với bộ lọc</p>
      }
    }
  `
})
export class DashboardComponent {
  items = signal<Item[]>([]);
  filter = signal('');
  isLoading = signal(true);
  hasError = signal(false);
  errorMessage = signal('');

  // Computed signal chỉ tính lại khi dependencies thay đổi
  filteredItems = computed(() =>
    this.items().filter(item =>
      item.name.toLowerCase().includes(this.filter().toLowerCase())
    )
  );
}

Khi signal cập nhật, Angular chỉ đánh giá lại các block control flow bị ảnh hưởng. Kết hợp với change detection zoneless của Angular, điều này cho phép rendering được tối ưu hóa cao.

Câu Hỏi Phỏng Vấn Phổ Biến Về Control Flow Angular

Các cuộc phỏng vấn kỹ thuật thường kiểm tra hiểu biết về cú pháp control flow, đặc biệt là sự khác biệt với structural directive.

Câu Hỏi 1: Tại sao track bắt buộc trong @for?

Biểu thức track cung cấp cho Angular một identity ổn định cho mỗi item. Không có nó, Angular không thể xác định hiệu quả node DOM nào cần tạo, cập nhật, hoặc hủy khi collection thay đổi. Bắt buộc track buộc developer phải đưa ra quyết định rõ ràng về identity, tránh các vấn đề hiệu năng của reference tracking implicit.

Câu Hỏi 2: @if khác *ngIf với async pipe như thế nào?

Cả hai đều có thể xử lý observable, nhưng @if với signals cung cấp truy cập đồng bộ vào giá trị:

typescript
// Với *ngIf và async pipe (legacy)
<div *ngIf="user$ | async as user">{{ user.name }}</div>

// Với @if và signals (hiện đại)
@if (user(); as user) {
  <div>{{ user.name }}</div>
}

Cách tiếp cận signal tránh quản lý subscription và tích hợp tốt hơn với change detection của Angular.

Câu Hỏi 3: @switch có thể thay thế chuỗi @if/@else if phức tạp không?

@switch nên được sử dụng khi so sánh một biểu thức duy nhất với nhiều giá trị rời rạc. Cho các điều kiện boolean phức tạp liên quan đến nhiều biểu thức khác nhau, @if/@else if vẫn phù hợp hơn:

html
<!-- Sử dụng @switch cho biểu thức đơn, nhiều giá trị -->
@switch (status()) {
  @case ('active') { ... }
  @case ('inactive') { ... }
}

<!-- Sử dụng @if cho nhiều biểu thức -->
@if (isAdmin() && hasPermission('write')) {
  ...
} @else if (isEditor()) {
  ...
}

Câu Hỏi 4: Điều gì xảy ra với @for khi collection rỗng?

Khi iterable không chứa item nào, Angular bỏ qua hoàn toàn nội dung @for. Block @empty tùy chọn render nội dung fallback trong trường hợp này. Không có @empty, không có gì được render, điều này khác với một số framework render container rỗng.

Migration Từ Structural Directive Sang Control Flow

Angular CLI cung cấp schematic migration tự động:

bash
# Migrate toàn bộ dự án
ng generate @angular/core:control-flow

# Migrate thư mục cụ thể
ng generate @angular/core:control-flow --path=src/app/features

Schematic xử lý hầu hết các transformation tự động, nhưng kiểm tra output cho các trường hợp đặc biệt:

Structural DirectiveControl Flow Tương Đương
*ngIf="condition"@if (condition) { }
*ngIf="condition; else elseBlock"@if (condition) { } @else { }
*ngFor="let item of items"@for (item of items; track item) { }
*ngFor="let item of items; index as i"@for (item of items; track item; let i = $index) { }
[ngSwitch] + *ngSwitchCase@switch + @case

Sau khi migration, xóa import CommonModule khỏi standalone components không còn sử dụng structural directive.

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Điểm Chính Về Cú Pháp Control Flow Angular

  • Cú pháp control flow Angular (@if, @for, @switch) là cách tiếp cận được khuyến nghị từ Angular 19, thay thế structural directive
  • Biểu thức track trong @for là bắt buộc: sử dụng identifier duy nhất như item.id để có hiệu năng tối ưu, tránh track item gây ra re-render dựa trên reference
  • @if hỗ trợ native nhánh @else if@else không cần ng-template, và keyword as tạo alias giá trị truthy để tái sử dụng
  • @switch sử dụng strict equality và không có fallthrough: các câu lệnh @case liên tiếp nhắm vào cùng block, và @default never; cho phép kiểm tra type exhaustive
  • Các block control flow tích hợp với Signals cho reactivity fine-grained: chỉ các block bị ảnh hưởng được re-render khi signals cập nhật
  • Chạy ng generate @angular/core:control-flow để tự động migrate template hiện có
Thử thách hôm nay

Bạn có tìm ra lỗi trong Angular không?

Một đoạn mã thật, một lỗi ẩn, mỗi ngày một lượt. Không cần tài khoản để thử.

Anthony Fillion-Maillet

Viết bởi

Anthony Fillion-Maillet

Người sáng lập SharpSkill

Lập trình viên fullstack hơn 10 năm. Anh điều hành SharpSkill và chịu trách nhiệm về mọi nội dung đăng tại đây.

Cập nhật ngày 3 tháng 9, 2026

Thẻ

#angular
#control-flow
#template-syntax
#interview

Chia sẻ

Bài viết liên quan