Capítulo 13: Reactive Forms Estritos, FormBuilder e Validações Assíncronas
Capítulo 13: Reactive Forms Estritos, FormBuilder e Validações Assíncronas
Especialização em Front-End com Angular • Angular 18+ • Signals, Standalone Architecture, RxJS, NgRx e SSR
🗺️ Mapa Conceitual da Arquitetura de Reactive Forms Tipados
flowchart TD
FormModel["FormGroup Tipado (NonNullableFormBuilder)"] --> Controls["Controles de Entrada (FormControl)"]
subgraph CONTROLS["Estrutura Interna de Controles"]
C1["nome: FormControl<string>"]
C2["email: FormControl<string>"]
C3["habilidades: FormArray<FormControl<string>>"]
end
Controls --- CONTROLS
subgraph VALIDATION_ENGINE["Motor de Validação Síncrona e Assíncrona"]
V1["Validação Síncrona Imediata (Validators.required, email, minLength)"]
V2["Validação Assíncrona (AsyncValidatorFn - Checagem de Backend)"]
end
CONTROLS --> VALIDATION_ENGINE
VALIDATION_ENGINE -->|"Calcula Estado"| Status["Status do Formulário (valid, invalid, pending, dirty, touched)"]
Status --> TemplateView["Template HTML com Feedback Visual Instantâneo"]
style FormModel fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style Controls fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style VALIDATION_ENGINE fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style Status fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
🏛️ 1. Formulários Reativos Tipados (Strictly Typed Forms)
No desenvolvimento corporativo de front-end, formulários são o ponto crítico de entrada e validação de dados de negócio. O Angular oferece o módulo ReactiveFormsModule, com um modelo orientado a objetos tipado pelo TypeScript que garante que nenhum campo tenha tipos incorretos ou propriedades fantasmas.
1.1 Vantagens dos Reactive Forms Tipados
- Type-Safety em Tempo de Compilação: O TypeScript impede acessar campos que não existem (
form.controls.campoInexistenteacusa erro no editor). - Controle Total de Valores Nulos: Com o
NonNullableFormBuilder, o método.reset()restaura o formulário para os valores padrão iniciais, em vez de preencher tudo comnull. - Rastreamento de Estado Imutável: Streams observáveis nativas (
valueChangesestatusChanges) permitem reagir em tempo real a qualquer alteração de digitação.
⚙️ 2. Construção de Formulário com NonNullableFormBuilder
Veja a implementação de um formulário corporativo completo com validação de formato e array dinâmico de itens:
import { Component, inject } from '@angular/core';
import { NonNullableFormBuilder, Validators, ReactiveFormsModule, FormArray, FormControl } from '@angular/forms';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-cadastro-fornecedor',
standalone: true,
imports: [ReactiveFormsModule, CommonModule],
template: `
<form [formGroup]="form" (ngSubmit)="aoSalvar()" class="form-container">
<h2>Cadastro de Fornecedor</h2>
<!-- Campo Razão Social -->
<div class="campo">
<label>Razão Social:</label>
<input formControlName="razaoSocial" type="text" />
@if (form.controls.razaoSocial.invalid && form.controls.razaoSocial.touched) {
<span class="erro">A Razão Social é obrigatória (mínimo 3 caracteres).</span>
}
</div>
<!-- Campo E-mail Comercial -->
<div class="campo">
<label>E-mail Comercial:</label>
<input formControlName="email" type="email" />
@if (form.controls.email.errors?.['required'] && form.controls.email.touched) {
<span class="erro">O e-mail é obrigatório.</span>
}
@if (form.controls.email.errors?.['email']) {
<span class="erro">Insira um e-mail válido.</span>
}
</div>
<!-- FormArray: Telefones de Contato -->
<div formArrayName="telefones" class="array-container">
<h3>Telefones de Contato</h3>
@for (tel of telefones.controls; track $index; let i = $index) {
<div class="linha-tel">
<input [formControlName]="i" placeholder="(XX) XXXXX-XXXX" />
<button type="button" (click)="removerTelefone(i)">Remover</button>
</div>
}
<button type="button" (click)="adicionarTelefone()">+ Adicionar Telefone</button>
</div>
<button type="submit" [disabled]="form.invalid">Cadastrar Fornecedor</button>
</form>
`
})
export class CadastroFornecedorComponent {
private fb = inject(NonNullableFormBuilder);
// Formulário estritamente tipado
form = this.fb.group({
razaoSocial: ['', [Validators.required, Validators.minLength(3)]],
email: ['', [Validators.required, Validators.email]],
telefones: this.fb.array([
this.fb.control('', [Validators.required])
])
});
get telefones(): FormArray<FormControl<string>> {
return this.form.controls.telefones;
}
adicionarTelefone(): void {
this.telefones.push(this.fb.control('', [Validators.required]));
}
removerTelefone(index: number): void {
if (this.telefones.length > 1) {
this.telefones.removeAt(index);
}
}
aoSalvar(): void {
if (this.form.valid) {
// .getRawValue() extrai o objeto JavaScript 100% tipado
const dados = this.form.getRawValue();
console.log('Fornecedor pronto para envio:', dados);
} else {
this.form.markAllAsTouched(); // Destaca erros visuais para o usuário
}
}
}
🔍 3. Validadores Customizados: Síncronos e Assíncronos
3.1 Validador Síncrono: Validação de CPF/CNPJ ou Regras de Negócio
Um validador síncrono é uma função que recebe um AbstractControl e retorna um mapa de erros (ValidationErrors) ou null caso esteja válido:
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function validarDominioCorporativo(dominioPermitido: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const email = String(control.value || '');
if (!email) return null; // Não valida campo vazio; deixe isso para Validators.required
const sufixo = `@${dominioPermitido}`;
return email.endsWith(sufixo) ? null : { dominioInvalido: { esperado: sufixo } };
};
}
3.2 Validador Assíncrono: Checagem de E-mail Único no Backend
Validações assíncronas retornam um Observable<ValidationErrors | null>. Elas aguardam a resposta de uma requisição HTTP:
import { inject, Injectable } from '@angular/core';
import { AsyncValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms';
import { HttpClient } from '@angular/common/http';
import { Observable, of, timer } from 'rxjs';
import { switchMap, map, catchError } from 'rxjs/operators';
export function checarEmailDisponivel(http: HttpClient): AsyncValidatorFn {
return (control: AbstractControl): Observable<ValidationErrors | null> => {
if (!control.value) return of(null);
// Debounce manual de 400ms para não sobrecarregar o servidor a cada tecla digitada
return timer(400).pipe(
switchMap(() => http.get<{ disponivel: boolean }>(`/api/usuarios/checar-email?email=${control.value}`)),
map(res => res.disponivel ? null : { emailJaEmUso: true }),
catchError(() => of(null)) // Se a API falhar, não bloqueia o formulário
);
};
}
⚖️ 4. Matriz de Estados de um Controle do Angular
| Propriedade Booleana | Descrição e Comportamento |
|---|---|
control.valid |
O controle passou por todas as regras de validação síncronas e assíncronas. |
control.invalid |
Pelo menos uma validação falhou; os detalhes estão em control.errors. |
control.pending |
Uma validação assíncrona está em execução no momento (exibindo spinner de carregamento). |
control.pristine |
O valor do campo nunca foi modificado pelo usuário desde a inicialização. |
control.dirty |
O usuário alterou o valor do campo no DOM. |
control.untouched |
O usuário ainda não clicou nem saiu do campo de input. |
control.touched |
O campo perdeu o foco (blur), momento padrão para exibir alertas de erro na UI. |
⚠️ 5. Armadilhas Comuns e Anti-Patterns
- Exibir Erros Imediatamente Antes da Interação do Usuário:
- Erro: Renderizar mensagens de erro vermelhas assim que o usuário abre a página.
- Correção: Condicione a exibição a
control.invalid && control.touchedou submissão do formulário.
- Utilizar
form.valueem vez deform.getRawValue():- Problema: Se um controle estiver desabilitado (
disabled: true),form.valueo exclui do objeto resultante! - Correção: Use
form.getRawValue()para garantir a inclusão de todos os campos desabilitados na submissão.
- Problema: Se um controle estiver desabilitado (
🛡️ 6. Boas Práticas e Arquitetura de Formulários
- Centralize Validadores Reutilizáveis: Crie uma pasta
shared/validatorscom validadores puros testados unitariamente. - Integração com Signals: Utilize
toSignal(this.form.valueChanges)quando precisar conectar a digitação do formulário a signals ecomputed().
📋 7. Cheat-Sheet: Resumo Executivo
const fb = inject(NonNullableFormBuilder);
const form = fb.group({
nome: ['', [Validators.required, Validators.minLength(3)]],
idade: [18, [Validators.min(18)]]
});
// Acesso seguro:
const nomeCtrl = form.controls.nome;
// Objeto final:
const dadosLimpos = form.getRawValue();