Capítulo 03: Control Flow Syntax Moderna (@if, @for, @switch) e Diretivas

Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR


🗺️ Mapa Conceitual do Novo Controle de Fluxo Embutido

flowchart TD
    A["Template HTML com Nova Sintaxe (@)"] --> B["Compilador Angular 18 (@angular/compiler)"]

    subgraph BLOCOS_EMBUTIDOS["Sintaxe de Controle de Fluxo Nativa"]
        C["@if (condicao) { ... } @else { ... }"]
        D["@for (item of items; track item.id) { ... } @empty { ... }"]
        E["@switch (status) { @case ('A') { ... } @default { ... } }"]
    end

    B --> BLOCOS_EMBUTIDOS

    C --> F["Zero overhead de diretivas micro-sintáticas (*ngIf)"]
    D --> G["Algoritmo de Reconciliação com track obrigatório"]
    E --> H["Avaliação direta sem containers auxiliares (*ngSwitch)"]

    F & G & H --> I["Compilação em Bytecode de Template Ultra-Rápido"]

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style BLOCOS_EMBUTIDOS fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style I fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. A Nova Sintaxe de Controle de Fluxo Nativa

No Angular 17 e consolidado no Angular 18, as diretivas estruturais legadas (*ngIf, *ngFor e *ngSwitch) foram descontinuadas em favor de uma Sintaxe de Controle de Fluxo Embutida no Compilador (Built-in Control Flow), identificada pelo prefixo @.

1.1 Por que o Angular Abandonou as Diretivas Estruturais?

As diretivas com asterisco (*ngIf) funcionavam como syntactic sugar que expandia o HTML em nós <ng-template> nos bastidores. Esse mecanismo gerava três problemas graves:

  1. Obrigatoriedade de Importar CommonModule: Nenhum componente podia fazer um simples if sem importar módulos inteiros.
  2. Performance Inferior: O compilador precisava instanciar diretivas e contextos de visualização (ViewContainerRef) para cada repetição.
  3. Erros Frequentes de Reconciliação: No *ngFor, a função trackBy era opcional. Quando os desenvolvedores esqueciam de utilizá-la, o Angular recriava 100% dos nós do DOM a cada pequena alteração de array.

1.2 O Bloco @if e Encadeamentos @else if / @else

O @if oferece uma estrutura declarativa e elegante, com inferência de tipos estreitada (type narrowing) que elimina checagens de null:

@if (user(); as u) {
  <p>Bem-vindo, {{ u.name }}!</p>
} @else if (isLoading()) {
  <p>Carregando perfil...</p>
} @else {
  <p>Usuário deslogado.</p>
}

1.3 O Bloco @for: track Obrigatório e Bloco @empty

No @for, o compilador do Angular obriga o desenvolvedor a declarar uma expressão de rastreamento (track item.id):

  • Otimização Extrema de DOM: O Angular utiliza a chave declarada em track para reutilizar elementos físicos no navegador, resultando em ganhos de performance de até 90% em listas grandes.
  • Variáveis Contextuais Embutidas: $index (índice 0-based), $count (tamanho total), $first (booleano se é o primeiro), $last (booleano se é o último), $even (booleano se é par), $odd (booleano se é ímpar).
  • Bloco @empty Integrado: Renderizado automaticamente caso o array seja nulo ou possua comprimento zero, eliminando blocos *ngIf secundários de empty-state.

1.4 O Bloco @switch

Substitui a combinação prolixa de [ngSwitch], *ngSwitchCase e *ngSwitchDefault por uma sintaxe limpa inspirada no TypeScript moderno:

@switch (pedido.status) {
  @case ('PAGO') { <span class="badge-verde">Pago</span> }
  @case ('PENDENTE') { <span class="badge-amarela">Aguardando</span> }
  @default { <span class="badge-cinza">Indefinido</span> }
}

⚖️ 2. Tabela Comparativa: Diretivas Estruturais Legadas vs Modern Control Flow

Recurso Técnico Diretivas Legadas (*ngIf, *ngFor) Modern Built-in Control Flow (@if, @for)
Dependência de Módulo Exige CommonModule no array imports. Zero dependências (embutido no compilador).
Rastreamento de Itens trackBy: fn (Opcional, frequentemente esquecido). track expr Obrigatório em tempo de compilação.
Tratamento de Lista Vazia Exigia <div *ngIf="items.length === 0"> manual. Bloco nativo @empty integrado.
Suporte a else if Não suportado (exigia múltiplos *ngIf complexos). Suporte nativo @else if.
Velocidade de Reconciliação Moderada. Até 90% mais rápida no cálculo de diffing.

⚠️ 3. Armadilhas Comuns em Produção (Gotchas & Anti-Patterns)

Anti-Pattern 1: Usar track $index em Listas com Mutações Intermediárias

Erro Comum: Declarar @for (item of items; track $index). Se a lista sofrer inserção no topo, ordenação ou deleção, o índice muda de referência e o Angular reaproveitará nós com dados incorretos. Abordagem Correta: Use sempre uma chave identificadora única e estável do modelo de dados: track item.id ou track item.uuid.

Anti-Pattern 2: Esquecer Parênteses ao Ler Sinais dentro de Blocos @if

Erro Comum: Escrever @if (isLoading) quando isLoading é um Signal (signal(false)). Como o Signal é uma função, a referência da função é truthy, avaliando a condição incorretamente como verdadeira. Abordagem Correta: Invoque o Signal: @if (isLoading()).


💻 4. Código Conceitual Arquitetural Comentado

// src/app/components/cluster-nodes-table.component.ts
import { Component, signal } from '@angular/core';

interface NodeSummary {
  id: string;
  name: string;
  role: 'MASTER' | 'WORKER' | 'INGRESS';
  isReady: boolean;
}

@Component({
  selector: 'app-cluster-nodes-table',
  standalone: true, // Não precisa de CommonModule para @if e @for!
  template: `
    <div class="max-w-xl mx-auto p-5 bg-white border border-slate-200 rounded-2xl shadow-sm space-y-4">
      <div class="flex justify-between items-center border-b pb-3">
        <h3 class="font-bold text-slate-800 text-sm">Nós do Cluster (Novo Control Flow)</h3>
        <button
          type="button"
          (click)="nodes.set([])"
          class="text-xs text-red-500 hover:text-red-700 underline"
        >
          Simular Lista Vazia (@empty)
        </button>
      </div>

      <!-- NOVO CONTROL FLOW @for COM track ESTÁVEL E BLOCO @empty -->
      <div class="border rounded-xl divide-y overflow-hidden text-xs">
        @for (node of nodes(); track node.id; let idx = $index; let count = $count) {
          <div class="p-3 flex justify-between items-center hover:bg-slate-50">
            <div>
              <span class="text-slate-400 font-mono mr-2">#{{ idx + 1 }}/{{ count }}</span>
              <strong class="text-slate-800">{{ node.name }}</strong>
            </div>

            <div class="flex items-center gap-2">
              <!-- NOVO CONTROL FLOW @switch -->
              @switch (node.role) {
                @case ('MASTER') {
                  <span class="px-2 py-0.5 rounded text-[10px] font-bold bg-purple-100 text-purple-800">MASTER</span>
                }
                @case ('INGRESS') {
                  <span class="px-2 py-0.5 rounded text-[10px] font-bold bg-blue-100 text-blue-800">INGRESS</span>
                }
                @default {
                  <span class="px-2 py-0.5 rounded text-[10px] font-bold bg-slate-100 text-slate-800">WORKER</span>
                }
              }

              <!-- NOVO CONTROL FLOW @if / @else -->
              @if (node.isReady) {
                <span class="text-emerald-600 font-bold">✓ Ready</span>
              } @else {
                <span class="text-red-600 font-bold">✕ NotReady</span>
              }
            </div>
          </div>
        } @empty {
          <!-- Bloco renderizado automaticamente quando a lista estiver vazia -->
          <div class="p-6 text-center text-slate-400">
            Nenhum nó registrado no cluster.
          </div>
        }
      </div>
    </div>
  `
})
export class ClusterNodesTableComponent {
  nodes = signal<NodeSummary[]>([
    { id: 'node-01', name: 'master-control-plane-01', role: 'MASTER', isReady: true },
    { id: 'node-02', name: 'worker-pool-compute-02', role: 'WORKER', isReady: true },
    { id: 'node-03', name: 'ingress-edge-router-03', role: 'INGRESS', isReady: false }
  ]);
}

🛠️ 5. Cheat-Sheet da Sintaxe de Controle de Fluxo

# Comando de migração automática do Angular CLI (converte todo o projeto para @if e @for)
ng g @angular/core:control-flow
<!-- Bloco @if / @else if / @else -->
@if (isAdmin()) {
  <AdminDashboard />
} @else if (isOperator()) {
  <OperatorPanel />
} @else {
  <LoginPrompt />
}

<!-- Bloco @for com variáveis de contexto -->
@for (item of items(); track item.id; let i = $index; let isLast = $last) {
  <div>{{ i }} - {{ item.nome }} {{ isLast ? '(Último)' : '' }}</div>
} @empty {
  <p>Nenhum item encontrado.</p>
}