Capítulo 05: Comunicação entre Componentes com Signal Inputs e Outputs
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:
- Falta de Reatividade Nativa: Alterações em
@Input()exigiam implementar o hook verbosongOnChangesou recorrer a getters/setters complexos. - Timing Inconsistente: Propriedades
@Input()não estavam prontas no momento da instanciação da classe (constructor), gerando valoresundefinedacidentais. - 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()einput.required()output()eoutputFromObservable()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
- Tentar Mudar o Valor de um
input()Diretamente:- Erro:
this.meuInput.set(10). O TypeScript emitirá erro de compilação porqueinput()retorna umSignalsomente-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 umsignal(this.meuInput())local.
- Erro:
- 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>.
- Erro:
🛡️ 7. Boas Práticas e Arquitetura de Componentes
- Smart vs Dumb Components (Container vs Presentational): Utilize
input()eoutput()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)