ไวยากรณ์ Control Flow ใน Angular 2026: @if, @for, @switch และคำถามสัมภาษณ์

เรียนรู้ไวยากรณ์ control flow ของ Angular (@if, @for, @switch) ที่มาแทนที่ structural directive ครบถ้วนพร้อมตัวอย่างโค้ด เคล็ดลับการ migrate และคำถามสัมภาษณ์เทคนิค

คู่มือไวยากรณ์ control flow Angular @if @for @switch

ไวยากรณ์ control flow ของ Angular (@if, @for, @switch) มาแทนที่ structural directive อย่าง *ngIf, *ngFor และ *ngSwitch ที่ครอบงำ template ของ Angular มาหลายปี ถูกแนะนำใน Angular 17 เสถียรตั้งแต่ Angular 18 และกลายเป็นค่าเริ่มต้นใน Angular 22 ไวยากรณ์แบบ block นี้ทำให้ template ใกล้เคียงกับโครงสร้างการเขียนโปรแกรมมาตรฐานมากขึ้น พร้อมทั้งเปิดโอกาสให้มีการปรับแต่งประสิทธิภาพได้ดียิ่งขึ้น

มี Migration ให้ใช้งาน

รันคำสั่ง ng generate @angular/core:control-flow เพื่อ migrate จาก structural directive ไปยังไวยากรณ์ control flow ใหม่โดยอัตโนมัติ ทั้งสองไวยากรณ์สามารถอยู่ร่วมกันได้ในระหว่างการ migrate แต่ ngIf, ngFor และ ngSwitch ถูก deprecated ตั้งแต่ Angular 19

ทำไม Angular ถึงเปลี่ยนจาก Structural Directive มาเป็นไวยากรณ์ Block

Structural directive ต้องการ import CommonModule ใช้ microsyntax ที่แตกต่างจาก JavaScript และบังคับให้นักพัฒนาต้องครอบเนื้อหาด้วย ng-template สำหรับเงื่อนไขที่ซับซ้อน ไวยากรณ์ control flow ใหม่แก้ปัญหาเหล่านี้:

  • ไม่ต้อง import: @if, @for และ @switch ถูก built-in เข้ากับ framework
  • ไวยากรณ์คล้าย JavaScript: เงื่อนไขและ loop อ่านได้เหมือนโค้ดมาตรฐาน
  • รองรับ @else แบบ native: ไม่ต้องอ้างอิง ng-template สำหรับ branch else อีกต่อไป
  • Tree-shaking ดีขึ้น: block control flow ที่ไม่ได้ใช้ไม่มีผลกระทบต่อขนาด bundle

เอกสาร Angular แนะนำให้นำไวยากรณ์ block มาใช้อย่างเต็มที่ในทุกโปรเจกต์ใหม่ และ migrate codebase ที่มีอยู่ทีละขั้น

@if: Conditional Rendering โดยไม่ต้องใช้ ng-template

Block @if render เนื้อหาตามเงื่อนไขโดยอิงจากนิพจน์ที่เป็น truthy ต่างจาก *ngIf ตรงที่รองรับ branch @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 ยังรองรับ aliasing ตัวแปรด้วย keyword as ซึ่งดึงค่าจากนิพจน์ที่ซ้อนกัน:

html
<!-- หลีกเลี่ยงการเข้าถึง property ซ้ำๆ -->
@if (user().profile?.settings?.theme; as theme) {
  <p>ธีมปัจจุบัน: {{ theme }}</p>
}

รูปแบบนี้มีประโยชน์โดยเฉพาะเมื่อทำงานกับข้อมูล async หรือ object ที่ซ้อนลึก เนื่องจากตัวแปรที่ถูก alias จะถูกกำหนดเฉพาะเมื่อเงื่อนไขเป็น truthy

@for: Iteration พร้อม Track Expression ที่บังคับ

Block @for iterate ผ่าน iterable ใดๆ ของ JavaScript พร้อมการปรับแต่งสำหรับ array ต่างจาก *ngFor ตรงที่ต้องมีนิพจน์ 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>ไม่มีสินค้า</p>
    }
  `
})
export class ProductListComponent {
  products = input.required<Product[]>();
}

นิพจน์ track บอก Angular ว่าจะระบุตัวตนของแต่ละ item ในแต่ละ re-render อย่างไร การเลือก property tracking ที่ถูกต้องส่งผลโดยตรงต่อประสิทธิภาพ:

นิพจน์ Trackกรณีใช้งานผลกระทบต่อประสิทธิภาพ
track item.idItem ที่มี identifier เฉพาะดีที่สุด: อัปเดต DOM น้อยที่สุด
track $indexรายการคงที่ที่ไม่เคยเรียงลำดับใหม่ยอมรับได้: re-render ทั้งหมดเมื่อเรียงลำดับใหม่
track itemTracking แบบ referenceแย่: reference ใหม่ = node DOM ใหม่

ตัวแปร Contextual ใน Block @for

Angular จัดเตรียมตัวแปร implicit ภายใน block @for ที่แสดง 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>
}

ตัวแปร contextual ที่มีให้:

  • $index: ตำแหน่งเริ่มจากศูนย์
  • $count: จำนวน item ทั้งหมด
  • $first, $last: flag boolean สำหรับ item แรก/สุดท้าย
  • $even, $odd: flag boolean ตามความเป็นคู่หรือคี่ของ index

พร้อมที่จะพิชิตการสัมภาษณ์ Angular แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

@switch: Conditional Branching แบบ Type-Safe

Block @switch ให้ pattern matching แบบ exhaustive ด้วยการเปรียบเทียบ 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">รอดำเนินการ</span>
      }
      @case ('processing') {
        <span class="badge badge-blue">กำลังดำเนินการ</span>
      }
      @case ('shipped') {
        <span class="badge badge-yellow">จัดส่งแล้ว</span>
      }
      @case ('delivered') {
        <span class="badge badge-green">ส่งถึงแล้ว</span>
      }
    }
  `
})
export class StatusBadgeComponent {
  status = input.required<OrderStatus>();
}

ต่างจาก switch ของ JavaScript ตรงที่ @switch ของ Angular ไม่มีพฤติกรรม fallthrough หลายเงื่อนไขที่เล็งไปยัง block เดียวกันต้องใช้คำสั่ง @case ติดต่อกัน:

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

การตรวจสอบ Type แบบ Exhaustive ด้วย @default never

Angular 22 รองรับการตรวจสอบ exhaustiveness ในเวลา compile การใช้ @default never; ประกาศว่าไม่ควรมี case ที่เหลืออยู่:

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

// ข้อผิดพลาด compile ถ้าค่า Theme ขาดหายไปจาก block @case
@switch (theme()) {
  @case ('light') { /* ... */ }
  @case ('dark') { /* ... */ }
  @case ('system') { /* ... */ }
  @default never;
}

การเพิ่มค่าใหม่เข้า union Theme จะทำให้เกิดข้อผิดพลาด compile บังคับให้นักพัฒนาต้องจัดการทุก case

ไวยากรณ์ Control Flow และการผสานรวมกับ Angular Signals

Block control flow ผสานรวมอย่างราบรื่นกับ Angular Signals ทำให้เกิด 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>ไม่มี item ที่ตรงกับตัวกรอง</p>
      }
    }
  `
})
export class DashboardComponent {
  items = signal<Item[]>([]);
  filter = signal('');
  isLoading = signal(true);
  hasError = signal(false);
  errorMessage = signal('');

  // Computed signal คำนวณใหม่เฉพาะเมื่อ dependencies เปลี่ยน
  filteredItems = computed(() =>
    this.items().filter(item =>
      item.name.toLowerCase().includes(this.filter().toLowerCase())
    )
  );
}

เมื่อ signal อัปเดต Angular จะประเมินเฉพาะ block control flow ที่ได้รับผลกระทบใหม่ เมื่อรวมกับ การตรวจจับการเปลี่ยนแปลงแบบ zoneless ของ Angular จะทำให้เกิดการ render ที่มีประสิทธิภาพสูง

คำถามสัมภาษณ์ทั่วไปเกี่ยวกับ Control Flow ของ Angular

การสัมภาษณ์เทคนิคมักทดสอบความเข้าใจเกี่ยวกับไวยากรณ์ control flow โดยเฉพาะความแตกต่างจาก structural directive

คำถามที่ 1: ทำไม track ถึงบังคับใน @for?

นิพจน์ track ให้ identity ที่เสถียรสำหรับแต่ละ item แก่ Angular หากไม่มี Angular จะไม่สามารถระบุได้อย่างมีประสิทธิภาพว่า node DOM ใดที่ต้องสร้าง อัปเดต หรือทำลายเมื่อ collection เปลี่ยน การบังคับ track ทำให้นักพัฒนาต้องตัดสินใจอย่างชัดเจนเกี่ยวกับ identity หลีกเลี่ยงปัญหาประสิทธิภาพของ reference tracking แบบ implicit

คำถามที่ 2: @if แตกต่างจาก *ngIf กับ async pipe อย่างไร?

ทั้งสองสามารถจัดการ observable ได้ แต่ @if กับ signals ให้การเข้าถึงค่าแบบ synchronous:

typescript
// กับ *ngIf และ async pipe (legacy)
<div *ngIf="user$ | async as user">{{ user.name }}</div>

// กับ @if และ signals (สมัยใหม่)
@if (user(); as user) {
  <div>{{ user.name }}</div>
}

แนวทาง signal หลีกเลี่ยงการจัดการ subscription และผสานรวมกับการตรวจจับการเปลี่ยนแปลงของ Angular ได้ดีกว่า

คำถามที่ 3: @switch สามารถแทนที่ chain @if/@else if ที่ซับซ้อนได้หรือไม่?

@switch ควรใช้เมื่อเปรียบเทียบนิพจน์เดียวกับหลายค่าแบบแยกกัน สำหรับเงื่อนไข boolean ที่ซับซ้อนที่เกี่ยวข้องกับนิพจน์หลายตัว @if/@else if ยังคงเหมาะสมกว่า:

html
<!-- ใช้ @switch สำหรับนิพจน์เดียว หลายค่า -->
@switch (status()) {
  @case ('active') { ... }
  @case ('inactive') { ... }
}

<!-- ใช้ @if สำหรับหลายนิพจน์ -->
@if (isAdmin() && hasPermission('write')) {
  ...
} @else if (isEditor()) {
  ...
}

คำถามที่ 4: เกิดอะไรขึ้นกับ @for เมื่อ collection ว่าง?

เมื่อ iterable ไม่มี item Angular จะข้ามเนื้อหา @for ทั้งหมด block @empty ที่เป็นทางเลือกจะ render เนื้อหา fallback ในกรณีนี้ หากไม่มี @empty จะไม่มีอะไร render ซึ่งแตกต่างจากบาง framework ที่ render container ว่าง

การ Migrate จาก Structural Directive ไปยัง Control Flow

Angular CLI จัดเตรียม schematic สำหรับ migration อัตโนมัติ:

bash
# Migrate ทั้งโปรเจกต์
ng generate @angular/core:control-flow

# Migrate directory ที่ระบุ
ng generate @angular/core:control-flow --path=src/app/features

Schematic จัดการ transformation ส่วนใหญ่โดยอัตโนมัติ แต่ตรวจสอบ output สำหรับกรณีพิเศษ:

Structural DirectiveControl Flow ที่เทียบเท่า
*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

หลังจาก migration ลบ import CommonModule จาก standalone components ที่ไม่ได้ใช้ structural directive อีกต่อไป

เริ่มฝึกซ้อมเลย!

ทดสอบความรู้ของคุณด้วยตัวจำลองสัมภาษณ์และแบบทดสอบเทคนิคครับ

ประเด็นสำคัญสำหรับไวยากรณ์ Control Flow ของ Angular

  • ไวยากรณ์ control flow ของ Angular (@if, @for, @switch) เป็นแนวทางที่แนะนำตั้งแต่ Angular 19 แทนที่ structural directive
  • นิพจน์ track ใน @for เป็นสิ่งบังคับ: ใช้ identifier เฉพาะเช่น item.id สำหรับประสิทธิภาพที่ดีที่สุด หลีกเลี่ยง track item ที่ทำให้เกิดการ re-render แบบ reference-based
  • @if รองรับ branch native @else if และ @else โดยไม่ต้องใช้ ng-template และ keyword as สร้าง alias ค่า truthy สำหรับใช้ซ้ำ
  • @switch ใช้ strict equality และไม่มี fallthrough: คำสั่ง @case ติดต่อกันเล็งไปยัง block เดียวกัน และ @default never; เปิดใช้งานการตรวจสอบ type แบบ exhaustive
  • Block control flow ผสานรวมกับ Signals สำหรับ reactivity แบบ fine-grained: เฉพาะ block ที่ได้รับผลกระทบเท่านั้นที่ re-render เมื่อ signals อัปเดต
  • รัน ng generate @angular/core:control-flow เพื่อ migrate template ที่มีอยู่โดยอัตโนมัติ
ชาเลนจ์ประจำวัน

คุณหาบั๊กใน Angular เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 3 กันยายน 2569

แท็ก

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

แชร์

บทความที่เกี่ยวข้อง