Identificação
- Duração: 2 horas
- Tipo: teoria aplicada e laboratório
- Entrega: biblioteca de componentes de tarefas
- Laboratório:
../exemplos/aula-4.3/index.html
Introdução
O que é composição de componentes?
Composição é construir uma tela juntando componentes menores. Em vez de concentrar lista, cartão, filtros e ações em um único arquivo, cada parte recebe uma responsabilidade clara.
TaskList (pai)
├── TaskCard (filho)
├── TaskCard (filho)
└── TaskCard (filho)
No Angular, um componente usa outro ao importá-lo e inserir seu seletor no template. A aplicação passa a formar uma árvore de componentes.
O que significam componente pai e componente filho?
Pai é o componente cujo template cria outro componente. Filho é a instância criada dentro desse template. Esses nomes descrevem a posição na árvore, não importância.
<app-task-card />
Se essa linha está no template de TaskList, então TaskList é o pai e
TaskCard é o filho.
Como os componentes conversam?
Usaremos um fluxo explícito:
dados: pai ──input──▶ filho
eventos: pai ◀─output── filho
O pai mantém o estado das tarefas. Cada filho recebe uma tarefa e informa ao pai quando o usuário solicita uma ação. O pai decide como atualizar o estado e envia os novos dados aos filhos.
O que é um input?
Input é uma propriedade pública pela qual um componente recebe dados de quem o usa.
Na API Angular atual, input() cria um InputSignal somente para leitura:
readonly position = input(0);
readonly task = input.required<Task>();
No template do pai, colchetes conectam uma expressão ao input:
<app-task-card [task]="task" [position]="$index" />
input.required<Task>() informa ao compilador que o pai precisa fornecer uma
tarefa. input(0) define zero como valor padrão.
Input é um atributo HTML?
Não necessariamente. task é parte da API do componente Angular. O binding
[task]="task" passa o valor da expressão; não transforma o objeto em texto no
HTML. Um atributo estático, como appearance="compact", passa texto quando existe
um input com esse nome.
Input é automaticamente bidirecional?
Não. O input comum leva dados do pai para o filho. Ele não cria sincronização de
volta. Para comunicar uma ação, o filho emite um output. O Angular também possui
model() para componentes que realmente precisam de two-way binding, como alguns
controles de formulário, mas esse não é o padrão para qualquer dado.
O que é um output?
Output é um evento personalizado da API de um componente. output<T>() define o
tipo do valor emitido:
readonly toggleRequested = output<number>();
requestToggle(): void {
this.toggleRequested.emit(this.task().id);
}
O pai escuta o evento com parênteses e recebe o valor em $event:
<app-task-card
[task]="task"
(toggleRequested)="toggleTask($event)"
/>
Output é igual a evento do navegador?
Os dois usam event binding no template, mas têm origens diferentes. (click) vem
do elemento DOM. (toggleRequested) foi definido pelo componente. Outputs Angular
personalizados não fazem bubbling pelo DOM; somente quem conectou o output recebe a
emissão.
Todo componente deve ter input e output?
Não. Um componente pode apenas apresentar conteúdo, controlar estado interno ou obter dados por um serviço. Inputs e outputs são usados quando fazem parte do contrato entre componentes.
@Input() e @Output() estão errados?
Não. Você encontrará estas APIs em muitos projetos:
@Input({ required: true }) task!: Task;
@Output() toggleRequested = new EventEmitter<number>();
Elas continuam suportadas. Para código novo, a documentação do Angular recomenda
as funções input() e output(), que serão o padrão deste curso.
Como isso entra na Knowledge AI?
A lista será dona das tarefas. Cada cartão receberá uma tarefa tipada e emitirá intenções de alternar ou remover. Assim, o cartão poderá ser reutilizado sem conhecer como a página armazena dados ou conversa com uma API.
Objetivos
Ao final da aula, o aluno deverá conseguir:
- explicar composição e árvore de componentes;
- identificar relações de pai e filho;
- importar um componente standalone;
- declarar inputs opcionais, padrão e obrigatórios;
- ler um
InputSignal; - declarar e emitir outputs tipados;
- receber o payload por
$event; - diferenciar evento DOM e output de componente;
- manter o estado no componente responsável;
- atualizar objetos e arrays de modo imutável.
Pré-requisitos
- Aulas 4.1 e 4.2 concluídas;
- componentes standalone e templates;
- bindings, eventos, signals e
@for; - interfaces, arrays e funções TypeScript.
Pergunta orientadora
Como dividir uma tela sem perder o controle sobre a origem dos dados e das ações?
Roteiro sugerido
| Etapa | Duração |
|---|---|
| Árvore e responsabilidades | 20 min |
| Inputs | 30 min |
| Outputs | 30 min |
| Fluxo e imutabilidade | 20 min |
| Laboratório | 15 min |
| Revisão | 5 min |
1. Comece pelas responsabilidades
Antes de criar arquivos, descreva o papel de cada componente:
| Componente | Responsabilidade | Não deve decidir |
|---|---|---|
TaskList |
manter, filtrar e atualizar tarefas | detalhes visuais do cartão |
TaskCard |
apresentar uma tarefa e capturar ações | armazenamento da lista |
Separar apenas para reduzir o tamanho do arquivo não basta. A fronteira deve ter um contrato compreensível.
2. Modelo compartilhado
export type TaskStatus = "todo" | "doing" | "done";
export interface Task {
readonly id: number;
readonly title: string;
readonly status: TaskStatus;
}
Pai e filho usam o mesmo contrato. readonly documenta que uma tarefa recebida não
deve ser alterada diretamente.
3. Criando o componente filho
import { Component, input, output } from "@angular/core";
import { Task } from "../task.model";
@Component({
selector: "app-task-card",
templateUrl: "./task-card.html",
styleUrl: "./task-card.css",
})
export class TaskCard {
readonly task = input.required<Task>();
readonly position = input(0);
readonly toggleRequested = output<number>();
readonly removeRequested = output<number>();
}
As propriedades formam a API pública do cartão.
4. Input obrigatório
readonly task = input.required<Task>();
O Angular verifica em tempo de compilação se o pai forneceu [task]. Não existe
valor inicial e, quando o contrato é atendido, a leitura não inclui undefined.
5. Input com valor padrão
readonly position = input(0);
O tipo number é inferido. Se o pai não passar o valor, o componente usa zero.
6. Input opcional sem padrão
readonly description = input<string>();
O tipo lido será string | undefined. O template e a classe precisam tratar a
ausência.
7. Lendo um InputSignal
<span>{{ position() + 1 }}</span>
<h3>{{ task().title }}</h3>
Inputs criados com input() são signals. Por isso, são lidos com (). Eles são
somente para leitura no filho: não chame set ou update.
8. Somente leitura não congela objetos
O InputSignal não permite substituir o valor, mas um objeto JavaScript ainda pode
ser mutado se o tipo permitir. Evite:
this.task().status = "done";
Além de contrariar readonly, isso esconde uma mudança no componente errado. Emita
uma intenção e deixe o pai criar o próximo estado.
9. Importando o filho no pai
@Component({
selector: "app-task-list",
imports: [TaskCard],
templateUrl: "./task-list.html",
})
export class TaskList {}
Componentes atuais são standalone por padrão. O import torna o seletor disponível no template daquele componente.
10. Passando dados do pai
@for (task of tasks(); track task.id; let position = $index) {
<app-task-card [task]="task" [position]="position" />
}
Cada repetição cria uma instância de TaskCard com valores próprios.
11. Valor estático e valor dinâmico
<app-badge appearance="compact" />
<app-task-card [position]="position" />
Sem colchetes, compact é texto literal. Com colchetes, position é avaliado como
expressão TypeScript do template.
12. Criando outputs tipados
readonly toggleRequested = output<number>();
readonly removeRequested = output<number>();
O tipo informa o payload exigido por emit e recebido pelo pai.
13. Emitindo uma intenção
requestToggle(): void {
this.toggleRequested.emit(this.task().id);
}
requestRemoval(): void {
this.removeRequested.emit(this.task().id);
}
O nome descreve algo que aconteceu ou foi solicitado. O cartão não precisa conhecer o array, o banco ou a API.
14. Do clique DOM ao output
<button type="button" (click)="requestToggle()">
Alternar status
</button>
O clique é um evento DOM recebido pelo filho. O método converte essa interação em um evento da API do componente.
15. O pai escuta o output
<app-task-card
[task]="task"
(toggleRequested)="toggleTask($event)"
(removeRequested)="removeTask($event)"
/>
Aqui, $event é o number emitido, não um objeto Event do navegador.
16. O pai atualiza o estado
toggleTask(id: number): void {
this.tasks.update((tasks) =>
tasks.map((task) =>
task.id === id
? { ...task, status: task.status === "done" ? "todo" : "done" }
: task,
),
);
}
O pai cria um novo array e um novo objeto para o item alterado. Depois, o binding atualiza o input da instância correspondente.
17. Fluxo completo
1. Pai fornece [task]
2. Filho apresenta task()
3. Pessoa clica no botão
4. Filho recebe (click)
5. Filho emite toggleRequested com o id
6. Pai recebe $event
7. Pai atualiza tasks imutavelmente
8. Angular atualiza o input do filho
Este ciclo deixa claro quem possui o estado e quem apenas pede uma mudança.
18. Estado local legítimo
Nem todo estado precisa subir ao pai. Um cartão pode manter algo estritamente visual, como estar expandido, desde que isso não seja informação de negócio necessária em outros componentes.
19. Evitando outputs genéricos
Evite nomes como clicked, changed ou event. Prefira o significado para quem
usa o componente:
readonly removalRequested = output<number>();
Nomes de outputs usam camelCase, não começam com on e não devem colidir com
eventos DOM como click.
20. Output não executa a regra do pai
O filho não recebe uma função arbitrária para chamar nem importa a classe do pai. Ele emite um evento. Isso reduz acoplamento e torna o contrato visível no template.
21. Quando considerar model()
model() é apropriado quando o próprio componente edita seu valor principal e deve
suportar [(value)], como um seletor ou controle de formulário. Em uma lista com
estado centralizado, input + output costuma expressar melhor a intenção.
22. Laboratório guiado
Abra o laboratório de fluxo entre componentes.
Etapa 1 — Observe a árvore
Relacione a lista pai aos cartões filhos.
Etapa 2 — Filtre no pai
O filtro muda quais filhos são compostos, sem alterar o contrato do cartão.
Etapa 3 — Solicite uma alternância
Clique em uma ação do cartão e acompanhe o registro: clique DOM, output, tratamento no pai e novo input.
Etapa 4 — Solicite uma remoção
O filho envia somente o identificador. O pai decide remover a tarefa.
Etapa 5 — Examine a fonte
Compare o componente pai, o filho e o template que conecta ambos.
23. Sobre o laboratório estático
O laboratório abre sem instalar Angular e reproduz a comunicação com JavaScript. A
pasta src/app contém a implementação Angular equivalente. A simulação não substitui
o compilador; ela torna o fluxo observável.
24. Erros comuns
Alterar o objeto recebido
O filho passa a controlar estado que pertence ao pai.
Esquecer os parênteses na leitura
task é o signal; task() é o valor atual.
Emitir o objeto inteiro sem necessidade
Um identificador costuma criar um contrato menor e mais previsível.
Confundir $event
Em (click), ele é um evento DOM. Em (toggleRequested), ele é o payload definido
pelo output.
Criar output com nome de evento nativo
Isso torna ambíguo se (click) pertence ao componente ou ao elemento host.
Subir todo estado para o topo
Estado visual sem compartilhamento pode permanecer no componente que o utiliza.
25. Boas práticas
- componha por responsabilidade;
- defina interfaces para dados compartilhados;
- marque inputs essenciais como obrigatórios;
- mantenha inputs recebidos somente para leitura;
- emita intenções tipadas;
- nomeie outputs em camelCase e sem prefixo
on; - mantenha o dono do estado responsável pela atualização;
- prefira atualizações imutáveis;
- mantenha o template como mapa visível da comunicação;
- não dependa de bubbling para outputs.
26. Exercícios
Exercício 1 — Input obrigatório
Crie UserAvatar com user = input.required<User>().
Exercício 2 — Input com padrão
Adicione size = input<"small" | "large">("small").
Exercício 3 — Output tipado
Emita userSelected contendo apenas o identificador.
Exercício 4 — Composição
Crie uma lista pai que repita avatares e trate a seleção.
Exercício 5 — Responsabilidade
Explique em qual componente deve ficar o usuário selecionado e por quê.
27. Desafio
Crie uma biblioteca de tarefas com:
- modelo compartilhado e somente para leitura;
- lista como dona do estado;
- cartão reutilizável;
- input obrigatório para tarefa;
- input com posição padrão;
- output de alternância;
- output de remoção;
- atualização imutável;
@forcomtrack task.id;- nomes que não colidam com eventos DOM.
28. Lista de verificação de conclusão
- Identifico pai e filho na árvore.
- Sei quando criar um componente.
- Importo um componente standalone.
- Declaro input padrão, opcional e obrigatório.
- Leio
InputSignalcom(). - Não altero diretamente dados recebidos.
- Declaro e emito output tipado.
- Recebo o payload em
$event. - Diferencio clique DOM e output Angular.
- Mantenho a atualização no dono do estado.
29. Critérios de avaliação
| Critério | Pontos |
|---|---|
| Divisão de responsabilidades | 15 |
| Inputs e contratos tipados | 25 |
| Outputs e payloads | 25 |
| Fluxo de dados | 15 |
| Atualização imutável | 10 |
| Clareza e acessibilidade | 10 |
| Total | 100 |
Resumo
Nesta aula, aprendemos que:
- composição transforma a aplicação em uma árvore de componentes;
- o pai fornece dados por inputs;
- o filho comunica intenções por outputs;
input()produz um signal somente para leitura;input.required()torna o contrato obrigatório;output<T>()cria um evento tipado;- outputs personalizados não fazem bubbling no DOM;
- o dono do estado realiza atualizações imutáveis;
@Input()e@Output()continuam suportados, mas as funções atuais são o padrão para código novo.
Próxima aula
Na Aula 4.4, retiraremos regras compartilhadas dos componentes e aprenderemos serviços e injeção de dependência.