Engenharia de IA Aplicada à Programação Web
Aula 6 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.6

Formulários e validação

2 horas Teoria + laboratório Angular
Ver fonte Markdown

Identificação

Introdução

O que é um formulário?

Formulário é uma região da interface usada para coletar dados e enviá-los para uma operação. Ele reúne campos, instruções, validações e uma ação de envio.

<form>
  <label for="title">Título</label>
  <input id="title" name="title">
  <button type="submit">Salvar</button>
</form>

Na Knowledge AI, o formulário transformará o que a pessoa digita em um rascunho de tarefa que depois será enviado à API.

O que é um campo?

Campo é um controle que recebe ou apresenta um valor: input, select, textarea, checkbox ou um componente de formulário personalizado. Cada campo deve ter nome, tipo, valor e finalidade claros.

Para que serve a label?

label identifica o campo. Ela ajuda todas as pessoas e é essencial para leitores de tela. Clicar na label também move o foco para o controle associado.

<label for="task-title">Título da tarefa</label>
<input id="task-title">

Placeholder não substitui label: ele desaparece ao digitar e não funciona como nome confiável do campo.

O que significa enviar o formulário?

Enviar é solicitar o processamento conjunto dos valores. Pressionar Enter em um campo ou ativar o botão type="submit" dispara o envio. Por isso, escutamos o evento do formulário, não apenas um clique específico.

O que é validação?

Validação verifica se os dados respeitam regras antes de uma operação. Exemplos:

  • título obrigatório;
  • título entre 5 e 80 caracteres;
  • prioridade entre as opções conhecidas;
  • data não pode estar no passado.

Validação não serve apenas para mostrar texto vermelho. Ela protege contratos e ajuda a pessoa a corrigir entradas.

Validar no navegador é suficiente?

Não. Existem camadas complementares:

HTML       → restrições básicas e semântica do controle
Angular    → retorno imediato e estado da interface
API NestJS → regra confiável antes de persistir
MySQL      → integridade final do armazenamento

Qualquer pessoa pode contornar o JavaScript do navegador. A API deve validar tudo novamente. O front-end nunca se conecta diretamente ao MySQL.

O que são Signal Forms?

Signal Forms são a API atual do Angular para criar formulários sobre um modelo gravável com signals. No Angular 22, essa API está estável e oferece sincronização, tipagem e validação baseada em schema.

readonly taskModel = signal<TaskDraft>({
  title: "",
  description: "",
  priority: "medium",
  dueDate: "",
});

readonly taskForm = form(this.taskModel);

O modelo é a fonte de verdade. O formulário cria uma árvore de campos com a mesma estrutura.

Signal Forms são a única forma de criar formulários Angular?

Não. O Angular oferece três abordagens:

Abordagem Fonte de verdade Uso típico
Signal Forms modelo em signal aplicações novas com Angular 22+
Reactive Forms FormControl e FormGroup sistemas existentes e formulários complexos
Template-driven propriedades e ngModel formulários simples

Esta aula ensina Signal Forms como caminho atual e apresenta Reactive Forms para leitura e manutenção de projetos existentes.

O que é um schema de validação?

É a declaração centralizada das regras associadas aos caminhos do modelo:

readonly taskForm = form(this.taskModel, (path) => {
  required(path.title, { message: "Informe o título." });
  minLength(path.title, 5, { message: "Use pelo menos 5 caracteres." });
});

O schema não é o schema do MySQL. Ele descreve comportamento do formulário no front-end.

Como isso entra na Knowledge AI?

Criaremos um cadastro com título, descrição, prioridade e prazo. O formulário mostrará erros depois da interação, acompanhará touched, dirty, valid e submitting, impedirá envio duplicado e representará sucesso ou erro da futura API.

Objetivos

Ao final da aula, o aluno deverá conseguir:

  1. explicar formulário, campo, label, valor, envio e validação;
  2. diferenciar validação HTML, Angular, API e banco;
  3. comparar Signal, Reactive e Template-driven Forms;
  4. criar um modelo tipado em signal;
  5. construir uma árvore com form();
  6. conectar controles com FormField;
  7. declarar regras em um schema;
  8. ler estados touched, dirty, valid, invalid e errors;
  9. apresentar mensagens acessíveis no momento adequado;
  10. processar envio sem duplicidade;
  11. reconhecer FormControl e FormGroup tipados;
  12. preparar os dados para a API sem confiar apenas no front-end.

Pré-requisitos

  • Aulas 4.1 a 4.5 concluídas;
  • componentes, templates, bindings e signals;
  • interfaces TypeScript;
  • serviços e injeção de dependência;
  • HTML semântico e acessibilidade básica.

Pergunta orientadora

Como coletar dados com segurança, boa experiência e um contrato tipado que continue válido no servidor?

Roteiro sugerido

Etapa Duração
Anatomia e camadas de validação 20 min
Modelo, campos e bindings 25 min
Schema e estados 30 min
Acessibilidade e envio 25 min
Laboratório 15 min
Revisão 5 min

1. Modelo do formulário

type TaskPriority = "low" | "medium" | "high";

interface TaskDraft {
  title: string;
  description: string;
  priority: TaskPriority;
  dueDate: string;
}

O modelo representa somente dados editáveis. Identificador, data de criação e usuário responsável normalmente são definidos fora deste formulário.

2. Valores iniciais definidos

const EMPTY_TASK: TaskDraft = {
  title: "",
  description: "",
  priority: "medium",
  dueDate: "",
};

Signal Forms usa objetos e arrays simples na estrutura. Inicialize todos os campos. undefined representa ausência de campo, não apenas um valor vazio.

3. Criando o modelo gravável

protected readonly taskModel = signal<TaskDraft>({ ...EMPTY_TASK });

O signal guarda a fonte de verdade. Alterações nos controles atualizam o modelo e alterações no modelo refletem nos controles.

4. Criando a árvore de campos

protected readonly taskForm = form(this.taskModel);

A árvore espelha o objeto:

taskForm
├── title
├── description
├── priority
└── dueDate

5. Importando as diretivas

import { form, FormField, FormRoot } from "@angular/forms/signals";

@Component({
  imports: [FormField, FormRoot],
})

FormField conecta controles à árvore. FormRoot coordena o envio do formulário.

6. Ligando um campo

<label for="task-title">Título</label>
<input
  id="task-title"
  type="text"
  [formField]="taskForm.title"
>

[formField] sincroniza valor e estados. Não precisamos implementar manualmente um manipulador de input para cada controle.

7. Ligando select e textarea

<select id="priority" [formField]="taskForm.priority">
  <option value="low">Baixa</option>
  <option value="medium">Média</option>
  <option value="high">Alta</option>
</select>

<textarea id="description" [formField]="taskForm.description"></textarea>

Os valores dos option precisam corresponder ao tipo aceito pelo modelo.

8. Schema de validação

protected readonly taskForm = form(this.taskModel, (path) => {
  required(path.title, { message: "Informe o título." });
  minLength(path.title, 5, { message: "Use pelo menos 5 caracteres." });
  maxLength(path.title, 80, { message: "Use no máximo 80 caracteres." });
});

O callback configura a lógica uma vez. As regras reagem às mudanças de valor.

9. Validadores incorporados

Signal Forms possui regras como:

  • required();
  • minLength() e maxLength();
  • min() e max();
  • email();
  • pattern();
  • validate() para regra personalizada.

Escolha a regra que expressa o contrato, não apenas a mensagem visual desejada.

10. Regra personalizada

validate(path.dueDate, ({ value }) => {
  const dueDate = value();
  if (!dueDate) return null;

  return dueDate < todayAsIsoDate()
    ? { kind: "pastDate", message: "O prazo não pode estar no passado." }
    : null;
});

Retorne um objeto de erro quando inválido e null quando válido. Compare datas no formato e fuso corretos para o domínio.

11. Estado de validação

taskForm.title().valid()
taskForm.title().invalid()
taskForm.title().errors()

Esses valores são signals. A raiz agrega os campos:

taskForm().valid()

12. Estado de interação

taskForm.title().touched()
taskForm.title().dirty()
  • touched: a pessoa focou e saiu do campo;
  • dirty: a pessoa modificou o campo;
  • untouched e pristine: estados opostos conceituais.

Um campo pode estar inválido antes de qualquer interação. Isso não significa que a mensagem deva aparecer imediatamente.

13. Quando mostrar erros

@if (taskForm.title().touched() && taskForm.title().invalid()) {
  <ul id="title-errors">
    @for (error of taskForm.title().errors(); track error.kind) {
      <li>{{ error.message }}</li>
    }
  </ul>
}

Depois de uma tentativa de envio, os campos inválidos também devem revelar seus erros. FormRoot e submit() ajudam marcando campos interativos como touched.

14. Associação acessível do erro

<input
  id="task-title"
  [formField]="taskForm.title"
  [attr.aria-invalid]="taskForm.title().invalid()"
  aria-describedby="title-help title-errors"
>
<p id="title-help">Entre 5 e 80 caracteres.</p>

aria-describedby conecta instruções e erros ao campo. aria-invalid comunica o estado; não substitui a mensagem textual.

15. Não dependa apenas de cor

Use texto, ícone textual ou estrutura além da borda vermelha. Pessoas com diferentes formas de percepção precisam compreender o problema sem distinguir uma cor.

16. Formulário raiz

<form [formRoot]="taskForm">
  <!-- campos -->
  <button type="submit">Salvar tarefa</button>
</form>

FormRoot previne o envio tradicional, configura novalidate e executa a ação de submissão definida no formulário.

17. Configurando a submissão

protected readonly taskForm = form(
  this.taskModel,
  taskSchema,
  {
    submission: {
      action: async (field) => {
        await this.taskStore.create(field().value());
      },
    },
  },
);

A ação só executa quando a validação permite. Enquanto aguarda, o estado de submissão fica ativo.

18. Impedindo envio duplicado

<button type="submit" [disabled]="taskForm().submitting()">
  @if (taskForm().submitting()) {
    Salvando...
  } @else {
    Salvar tarefa
  }
</button>

Desabilite durante a operação, não apenas porque o formulário ainda está inválido. Permitir a tentativa de envio ajuda a revelar erros para quem não percorreu todos os campos.

19. Foco após erro

Quando o envio falha, mova o foco para o primeiro campo inválido ou para um resumo de erros. Signal Forms oferece associação com o controle para ajudar a direcionar o foco. Não mova o foco a cada caractere digitado.

20. Sucesso e erro global

Mensagens devem usar regiões vivas com moderação:

<p role="status">Tarefa salva com sucesso.</p>
<p role="alert">Não foi possível salvar. Tente novamente.</p>

status é adequado para confirmação não urgente; alert para erro que exige atenção. Não use alert() do navegador como experiência principal.

21. Erros do servidor

A API pode rejeitar dados que passaram no front-end: título duplicado, permissão insuficiente ou conflito de versão. Mostre o erro no campo correspondente quando possível e mantenha um resumo global para falhas gerais.

Não substitua a mensagem do servidor cegamente. Mapeie códigos conhecidos para textos seguros e compreensíveis.

22. Reset consciente

Após sucesso, redefina o modelo apenas se isso fizer sentido para o fluxo. Em edição, navegar para o detalhe pode ser melhor. Em cadastro contínuo, limpar e focar o primeiro campo pode ser adequado.

Não apague os dados quando a API falhar.

23. HTML nativo continua importante

<input type="text" required minlength="5" maxlength="80">

Signal Forms espelha algumas restrições em atributos nativos para comportamento e acessibilidade. O estado suportado continua vindo da árvore do formulário, não de :invalid ou validationMessage do navegador.

24. novalidate não significa sem validação

Ele desliga a interface automática de validação do navegador para que o Angular controle mensagens e submissão consistentemente. As regras Angular e do servidor continuam obrigatórias.

25. Reactive Forms tipados

Em projetos existentes, você encontrará:

readonly taskForm = new FormGroup({
  title: new FormControl("", {
    nonNullable: true,
    validators: [Validators.required, Validators.minLength(5)],
  }),
  priority: new FormControl<TaskPriority>("medium", {
    nonNullable: true,
  }),
});

FormControl guarda valor e estado; FormGroup organiza controles. A tipagem estrita evita muitos casts e erros de nomes.

26. Template equivalente em Reactive Forms

<form [formGroup]="taskForm" (ngSubmit)="save()">
  <input formControlName="title">
  <select formControlName="priority"></select>
  <button type="submit">Salvar</button>
</form>

O componente importa ReactiveFormsModule. Não misture abordagens no mesmo formulário sem uma razão arquitetural clara.

27. Signal Forms ou Reactive Forms?

Use Signal Forms quando:

  • a aplicação é nova e usa Angular 22+;
  • signals já organizam o estado;
  • tipagem inferida pelo modelo é desejada;
  • schemas combinam com o projeto.

Use Reactive Forms quando:

  • a base existente já os utiliza;
  • há formulários dinâmicos complexos consolidados;
  • a equipe depende de APIs e bibliotecas reativas existentes;
  • estabilidade entre versões anteriores é necessária.

Consistência da base importa mais que trocar tecnologia por novidade.

28. Dados do formulário não são entidade pronta

interface TaskDraft {
  title: string;
  description: string;
  priority: TaskPriority;
  dueDate: string;
}

A API define autor, identificador e timestamps. Não envie campos administrativos simplesmente porque apareceram no objeto do navegador.

29. Laboratório guiado

Abra o cadastro acessível de tarefas.

Etapa 1 — Envie vazio

Observe o resumo e o foco no primeiro campo inválido.

Etapa 2 — Corrija cada campo

Acompanhe valor, touched, dirty e valid no painel de estado.

Etapa 3 — Teste a data

Informe uma data passada e observe a regra personalizada.

Etapa 4 — Simule falha da API

Confirme que os valores permanecem para correção ou nova tentativa.

Etapa 5 — Salve com sucesso

O botão representa submissão e evita uma segunda operação simultânea.

Etapa 6 — Examine a fonte

Compare modelo, schema, template e versão Reactive Forms.

30. Sobre o laboratório estático

O laboratório reproduz estados de um formulário para abrir sem instalar Angular. A pasta src/app contém Signal Forms equivalentes. A prévia usa APIs seguras do DOM e simula a resposta da futura API sem persistir dados.

31. Erros comuns

Usar placeholder como label

O campo perde seu nome visível durante a digitação.

Mostrar todos os erros ao abrir

A interface começa acusando uma pessoa que ainda não interagiu.

Validar somente no Angular

Requisições podem ser enviadas fora da interface.

Desabilitar sempre o botão inválido

A pessoa pode não descobrir por que não consegue avançar.

Apagar valores após erro da API

Isso obriga a redigitar e pode causar perda de trabalho.

Usar click em vez de submit

Enter e outras formas de envio deixam de funcionar corretamente.

Confiar no tipo TypeScript em ambiente de execução

Tipos desaparecem na execução. Dados externos precisam de validação real.

Misturar Signal e Reactive Forms sem necessidade

A fonte de verdade e os estados ficam difíceis de acompanhar.

32. Boas práticas

  • use elementos nativos e labels explícitas;
  • escolha tipos de input adequados;
  • mantenha um modelo de formulário próprio;
  • centralize regras no schema;
  • mostre erros após interação ou tentativa de envio;
  • associe ajuda e erro com aria-describedby;
  • não comunique erro apenas por cor;
  • permita envio para revelar problemas;
  • impeça duplicidade enquanto salva;
  • preserve valores em falhas;
  • valide novamente na API;
  • mantenha o MySQL inacessível ao navegador.

33. Exercícios

Exercício 1 — Labels

Crie campos de nome e e-mail com labels, ajuda e identificadores únicos.

Exercício 2 — Schema

Valide título obrigatório entre 5 e 80 caracteres.

Exercício 3 — Estados

Mostre mensagens apenas quando o campo estiver touched e invalid.

Exercício 4 — Regra personalizada

Impeça uma data de término anterior à data de início.

Exercício 5 — Reactive Forms

Reescreva dois campos com FormGroup e controles nonNullable.

34. Desafio

Crie um formulário de projeto com:

  • modelo tipado em signal;
  • pelo menos quatro campos;
  • labels e instruções;
  • FormField e FormRoot;
  • regras obrigatória, tamanho e personalizada;
  • mensagens após interação;
  • resumo no envio inválido;
  • foco no primeiro erro;
  • estado de submissão;
  • tratamento de erro de campo e global;
  • confirmação acessível;
  • mapeamento explícito para o DTO da API.

35. Lista de verificação de conclusão

  • Explico formulário, campo, label e envio.
  • Diferencio as camadas de validação.
  • Comparo as três abordagens Angular.
  • Crio modelo e árvore tipados.
  • Uso FormField e FormRoot.
  • Declaro schema de validação.
  • Leio estados de interação e validade.
  • Associo mensagens aos campos.
  • Trato envio inválido e duplicidade.
  • Reconheço Reactive Forms tipados.
  • Sei que a API precisa validar novamente.

36. Critérios de avaliação

Critério Pontos
Modelo e bindings tipados 20
Schema e regras 25
Estados e mensagens 20
Envio e tratamento de falhas 15
Acessibilidade 15
Organização 5
Total 100

Resumo

Nesta aula, aprendemos que:

  • formulários coletam dados como uma operação coerente;
  • label nomeia o campo e placeholder não a substitui;
  • validações do navegador, Angular, API e banco são complementares;
  • Signal Forms estão estáveis no Angular 22 para aplicações novas;
  • o modelo em signal é a fonte de verdade;
  • form() cria uma árvore tipada;
  • FormField conecta controles e FormRoot coordena o envio;
  • schemas centralizam regras;
  • estados orientam quando mostrar mensagens;
  • Reactive Forms continuam uma escolha sólida;
  • o servidor nunca deve confiar apenas na validação do front-end.

Próxima aula

Na Aula 4.7, conectaremos o serviço à API usando HTTP e RxJS, representando carregamento, sucesso, vazio e erro.

Fontes oficiais