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.campoInexistente acusa 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 com null.
  • Rastreamento de Estado Imutável: Streams observáveis nativas (valueChanges e statusChanges) 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

  1. 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.touched ou submissão do formulário.
  2. Utilizar form.value em vez de form.getRawValue():
    • Problema: Se um controle estiver desabilitado (disabled: true), form.value o exclui do objeto resultante!
    • Correção: Use form.getRawValue() para garantir a inclusão de todos os campos desabilitados na submissão.

🛡️ 6. Boas Práticas e Arquitetura de Formulários

  • Centralize Validadores Reutilizáveis: Crie uma pasta shared/validators com validadores puros testados unitariamente.
  • Integração com Signals: Utilize toSignal(this.form.valueChanges) quando precisar conectar a digitação do formulário a signals e computed().

📋 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();