Engenharia de IA Aplicada à Programação Web
Aula 3 de 10
Navegar por módulos e aulas
  1. 01 Fundamentos da Web
  2. 02 JavaScript moderno
  3. 03 TypeScript, Git e qualidade
  4. 04 Angular
  5. 05 Node.js, NestJS e APIs
  1. 4.1 Introdução ao Angular e componentes
  2. 4.2 Templates, bindings e control flow
  3. 4.3 Composição, inputs e outputs
  4. 4.4 Serviços e injeção de dependência
  5. 4.5 Rotas, layouts e navegação
  6. 4.6 Formulários e validação
  7. 4.7 HTTP, APIs e RxJS
  8. 4.8 Signals e estado da interface
  9. 4.9 Interceptors, erros e acessibilidade
  10. 4.10 Testes, compilação e projeto final

Módulo 4 · Aula 4.3

Composição, inputs e outputs

2 horas Teoria + laboratório Angular
Ver fonte Markdown

Identificação

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:

  1. explicar composição e árvore de componentes;
  2. identificar relações de pai e filho;
  3. importar um componente standalone;
  4. declarar inputs opcionais, padrão e obrigatórios;
  5. ler um InputSignal;
  6. declarar e emitir outputs tipados;
  7. receber o payload por $event;
  8. diferenciar evento DOM e output de componente;
  9. manter o estado no componente responsável;
  10. 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;
  • @for com track 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 InputSignal com ().
  • 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.

Fontes oficiais