Capítulo 05: Comunicação entre Componentes com Signal Inputs e Outputs

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


🗺️ Mapa Conceitual da Nova API de Componentes

flowchart TD
    Parent["Componente Pai (Smart / Container)"] -->|"Passagem de Propriedades [prop]='val' "| Child["Componente Filho (Presentational)"]
    Child -->|"(evento)='onAcao($event)' "| Parent

    subgraph SIGNAL_INPUTS["API Moderna de Comunicação (Angular 17.1+)"]
        A["input() / input.required()"]
        B["model() / model.required()"]
        C["output() / outputFromObservable()"]
    end

    Child --- SIGNAL_INPUTS

    A -->|"Leitura Reativa como Signal"| D["signal.value() & computed()"]
    B -->|"Two-Way Data Binding Automático"| E["[(prop)]='val' via WritableSignal"]
    C -->|"Emissão de Eventos Tipados"| F["output.emit(payload)"]

    style Parent fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style Child fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style SIGNAL_INPUTS fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style E fill:#e8f5e9,stroke:#4caf50,stroke-width:2px

🏛️ 1. O Paradigma Moderno de Comunicação no Angular 18+

Historicamente, o Angular dependia de decoradores TypeScript (@Input() e @Output()) para transferir dados e emitir eventos entre componentes. Esse modelo tradicional apresentava sérias desvantagens:

  1. Falta de Reatividade Nativa: Alterações em @Input() exigiam implementar o hook verboso ngOnChanges ou recorrer a getters/setters complexos.
  2. Timing Inconsistente: Propriedades @Input() não estavam prontas no momento da instanciação da classe (constructor), gerando valores undefined acidentais.
  3. Overhead de Reconciliação: O motor precisava percorrer a árvore inteira para identificar mutações em nós filhos.

A partir do Angular 17.1 e padronizado no Angular 18/19, o time do Angular introduziu as funções reativas:

  • input() e input.required()
  • output() e outputFromObservable()
  • model() (Two-way binding de primeira classe baseado em Signals)

⚙️ 2. Signal Inputs: Tipagem Estrita e Valores Derivados

Diferente de uma propriedade comum, o retorno de input() é um Signal somente-leitura (InputSignal<T>). Para ler seu valor dentro do template ou no TypeScript, utiliza-se a sintaxe de chamada de função meuInput().

import { Component, input, computed } from '@angular/core';

@Component({
  selector: 'app-user-card',
  standalone: true,
  template: `
    <div class="user-card" [class.premium]="isVip()">
      <h3>{{ nome() }} ({{ idade() }} anos)</h3>
      <p>Status: {{ statusFormatado() }}</p>
    </div>
  `
})
export class UserCardComponent {
  // Input obrigatório (gera erro em tempo de compilação se omitido pelo pai)
  nome = input.required<string>();

  // Input opcional com valor padrão inicial
  idade = input<number>(18);

  // Input com transformação de dados embutida (Transform function)
  isVip = input<boolean, boolean | string>(false, {
    transform: (value: boolean | string) => typeof value === 'string' ? value === 'true' : value
  });

  // Signal derivado computado automaticamente quando o input muda
  statusFormatado = computed(() => {
    return this.isVip() ? '⭐ Membro VIP' : 'Membro Padrão';
  });
}

2.1 Passagem de Dados pelo Componente Pai

No template do componente pai:

<app-user-card [nome]="usuarioAtual().nome" [idade]="usuarioAtual().idade" [isVip]="true" />

📡 3. A Nova API output()

A função output<T>() substitui integralmente o decorador @Output() e a classe EventEmitter. Ela é mais leve e possui tipagem muito mais segura.

import { Component, input, output } from '@angular/core';

export interface ProdutoItem {
  id: number;
  nome: string;
  preco: number;
}

@Component({
  selector: 'app-produto-row',
  standalone: true,
  template: `
    <div class="linha-produto">
      <span>{{ item().nome }} - {{ item().preco | currency:'BRL' }}</span>
      <button (click)="aoRemover()">Excluir</button>
      <button (click)="aoSelecionar()">Ver Detalhes</button>
    </div>
  `
})
export class ProdutoRowComponent {
  item = input.required<ProdutoItem>();

  // Outputs tipados usando a nova API
  remover = output<number>(); // Emite o ID do item
  selecionar = output<ProdutoItem>(); // Emite o objeto completo

  aoRemover(): void {
    this.remover.emit(this.item().id);
  }

  aoSelecionar(): void {
    this.selecionar.emit(this.item());
  }
}

🔄 4. Two-Way Binding Simplificado com model()

O Angular 18 revolucionou o two-way data binding com a introdução de model(). Ao declarar um model(), o Angular cria automaticamente um par de Input reativo + Output de evento com convenção de sufixo Change:

// Componente Filho: Switch / Alternador Reativo
import { Component, model } from '@angular/core';

@Component({
  selector: 'app-toggle-switch',
  standalone: true,
  template: `
    <button (click)="alternar()" [class.ativo]="ligado()">
      {{ ligado() ? 'LIGADO' : 'DESLIGADO' }}
    </button>
  `
})
export class ToggleSwitchComponent {
  // model() cria um WritableSignal sincronizado nos dois sentidos!
  ligado = model<boolean>(false);

  alternar(): void {
    this.ligado.update(estado => !estado);
  }
}

No componente pai, o desenvolvedor pode consumir via banana-in-a-box [(...)]:

<!-- Ligação bidirecional perfeita com o signal do pai -->
<app-toggle-switch [(ligado)]="notificacoesAtivas" />
<p>Estado no Pai: {{ notificacoesAtivas() ? 'Habilitado' : 'Desabilitado' }}</p>

⚖️ 5. Comparativo: Decoradores Legados vs Signal APIs

Característica @Input() Legado input() Reativo Moderno
Tipo de Dado Retornado Propriedade comum do JS (T) InputSignal<T>
Gatilho de Reação Hook ngOnChanges() ou set prop() computed() ou effect()
Obrigatoriedade { required: true } via metadata input.required<T>() em nível de compilação
Transformação { transform: booleanAttribute } transform: (val) => ... nativa
Two-Way Binding @Input() x + @Output() xChange manual model<T>() com apenas 1 linha
Performance de Detecção Reavalia o componente no ciclo Zone.js Mudança de granularidade fina com Signals

⚠️ 6. Armadilhas Comuns e Anti-Patterns

  1. Tentar Mudar o Valor de um input() Diretamente:
    • Erro: this.meuInput.set(10). O TypeScript emitirá erro de compilação porque input() retorna um Signal somente-leitura.
    • Solução: Se o componente precisa alterar e refletir o dado de volta ao pai, utilize model(). Se for uma mutação apenas local, inicialize um signal(this.meuInput()) local.
  2. Esquecer os Parênteses de Invocação no Template:
    • Erro: <p>{{ nome }}</p> renderizará function computeSignal() { ... } ou a referência do signal.
    • Solução: Sempre invoque o signal com parênteses: <p>{{ nome() }}</p>.

🛡️ 7. Boas Práticas e Arquitetura de Componentes

  • Smart vs Dumb Components (Container vs Presentational): Utilize input() e output() massivamente em componentes de apresentação (Dumb Components) para mantê-los 100% puros e fáceis de testar. Deixe a lógica de orquestração de serviços e regras de negócio para componentes Container (Smart).
  • Evitar Prop Drilling Profundo: Se um dado precisa atravessar 4 ou 5 níveis de componentes intermediários que não usam aquele dado, prefira um serviço injetável no nível do módulo ou um SignalStore compartilhado.

📋 8. Cheat-Sheet: Síntese da API

// Inputs
titulo = input<string>('Padrão');         // Opcional com valor padrão
id = input.required<number>();             // Obrigatório
ativo = input(false, { transform: booleanAttribute });

// Outputs
notificar = output<string>();              // Emite string: this.notificar.emit('ok')

// Model (Two-way)
quantidade = model<number>(1);             // Sincronização bidirecional: this.quantidade.set(2)