DEV Community

Davi Batista
Davi Batista

Posted on

Angular AsyncValidator: Validação Assíncrona de Formulários

Introdução

Validar formulários é essencial para manter a integridade dos dados enviados pelos usuários. Muitas vezes, esse processo exige consultas a serviços externos, seja para confirmar se um nome de usuário já está em uso, verificar em tempo real se um cupom de desconto ainda é válido e não ultrapassou seu limite de utilizações, ou até mesmo consultar o estoque de um produto antes de habilitar o botão “Adicionar ao carrinho”.

O AsyncValidator do Angular surge como a solução ideal para esses cenários, pois suporta retornos assíncronos via Promise ou Observable, garantindo que o formulário só seja considerado válido depois que todas as validações remotas forem concluídas com sucesso.


1. O que é um AsyncValidator? 🤔

  • É uma classe (ou função) que implementa a interface AsyncValidator do Angular.
  • Retorna um Observable<ValidationErrors | null> ou Promise<ValidationErrors | null>.
  • Ideal para checar duplicidade em servidor, disponibilidade de e-mail, validadores de segurança, etc.

2. Visão geral do fluxo

  1. Usuário digita o código do cupom no campo do formulário.
  2. Ao perder o foco (updateOn: 'blur'), o AsyncValidator é acionado.
  3. Dispara-se uma chamada HTTP simulada ao nosso serviço de cupons.
  4. Se o cupom for válido (true), o validador retorna null e o campo fica válido.
  5. Se o cupom for inválido (false), retorna { invalidCoupon: true } e exibe erro.
  6. Em caso de erro de rede, retorna null para não bloquear o avanço do usuário.

3. Serviço de integração

Não há razão para se preocupar com um backend real, vamos apenas simular uma chamada http para busca de dados:

// coupon.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class CouponService {
  // Lista de cupons válidos simulada
  private validCoupons = ['PROMO10', 'SAVE20', 'OFF30'];

  /** Simula uma chamada HTTP verificando se o cupom é válido */
  validateCoupon(code: string): Observable<boolean> {
    const isValid = this.validCoupons.includes(code.toUpperCase());

    return of(isValid).pipe(delay(500));
  }
}

Enter fullscreen mode Exit fullscreen mode

4. Criando o AsyncValidator customizado

// coupon.validator.ts
import { inject, Injectable } from '@angular/core';
import {
  AbstractControl,
  AsyncValidator,
  ValidationErrors,
} from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { CouponService } from './coupon.service';

@Injectable({ providedIn: 'root' })
export class CouponValidator implements AsyncValidator {
  private couponService = inject(CouponService);

  validate(control: AbstractControl): Observable<ValidationErrors | null> {
    const code = control.value;
    if (!code) {
      return of(null);
    }
    return this.couponService
      .validateCoupon(code)
      .pipe(map((isValid) => (isValid ? null : { invalidCoupon: true })));
  }
}

Enter fullscreen mode Exit fullscreen mode
  • timer(500): aguarda 500 ms após o último caractere.
  • switchMap: cancela requisições anteriores se o valor mudar.
  • catchError: devolve null em falha de rede para não invalidar o form.

5. Integrando no FormBuilder

// checkout.component.ts
import { Component, inject, OnInit } from '@angular/core';
import {
  FormBuilder,
  FormGroup,
  FormsModule,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { CouponValidator } from './coupon.validator';

@Component({
  selector: 'app-checkout',
  templateUrl: './checkout.component.html',
  styleUrls: ['./checkout.component.css'],
  imports: [FormsModule, ReactiveFormsModule],
})
export class CheckoutComponent implements OnInit {
  checkoutForm!: FormGroup;

  private fb = inject(FormBuilder);
  private couponValidator = inject(CouponValidator);

  ngOnInit(): void {
    this.checkoutForm = this.fb.group({
      couponCode: this.fb.control('', {
        validators: [Validators.required],
        asyncValidators: [
          this.couponValidator.validate.bind(this.couponValidator),
        ],
      }),
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

6. Exibindo erros no template

<!-- checkout.component.html -->
<form class="form-container" [formGroup]="checkoutForm">
  <div class="field-group">
    <label for="coupon" class="field-label">🎫 Cupom de Desconto</label>
    <input
      id="coupon"
      type="text"
      formControlName="couponCode"
      placeholder="Insira seu cupom"
      class="field-input"
    />

    @if(checkoutForm.get('couponCode')?.pending) {
    <div class="field-info">⏳ Verificando cupom...</div>
    }
    <!--  -->
    @if(checkoutForm.get('couponCode')?.hasError('required')) {
    <div class="field-info">
      Que tal usar o cupom de desconto <strong>OFF30</strong>?
    </div>
    }
    <!--  -->
    @if( checkoutForm.get('couponCode')?.valid &&
    !checkoutForm.get('couponCode')?.pending &&
    checkoutForm.get('couponCode')?.dirty ) {
    <div class="field-success">✅ Cupom aplicado com sucesso!</div>
    }
  </div>
</form>

Enter fullscreen mode Exit fullscreen mode
  • form.pending: indica que a validação assíncrona está em andamento.

7. Boas práticas 🎯

  • Use updateOn: 'blur' ou submit para reduzir chamadas excessivas.
  • Aplique debounce interno (com timer ou debounceTime).
  • Sempre trate erros de rede com catchError, para não travar o formulário.
  • Separe lógica de API em serviços (CouponService) e validação em classes (AsyncValidator), seguindo o princípio de responsabilidade única.

Conclusão

Com este padrão, você garante validações assíncronas limpas, reutilizáveis e fáceis de testar. O exemplo de validação de cupons é apenas uma amostra: a mesma abordagem vale para e-mails, CPFs.

Agora é sua vez: experimente adaptar esse AsyncValidator a outros cenários 😊

🌐 repository: https://github.com/DaviBatistaOliv/Angular-AsyncValidator-cupon

Top comments (0)