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>
ouPromise<ValidationErrors | null>
. - Ideal para checar duplicidade em servidor, disponibilidade de e-mail, validadores de segurança, etc.
2. Visão geral do fluxo
- Usuário digita o código do cupom no campo do formulário.
- Ao perder o foco (
updateOn: 'blur'
), oAsyncValidator
é acionado. - Dispara-se uma chamada HTTP simulada ao nosso serviço de cupons.
- Se o cupom for válido (
true
), o validador retornanull
e o campo fica válido. - Se o cupom for inválido (
false
), retorna{ invalidCoupon: true }
e exibe erro. - 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));
}
}
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 })));
}
}
-
timer(500)
: aguarda 500 ms após o último caractere. -
switchMap
: cancela requisições anteriores se o valor mudar. -
catchError
: devolvenull
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),
],
}),
});
}
}
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>
-
form.pending
: indica que a validação assíncrona está em andamento.
7. Boas práticas 🎯
- Use
updateOn: 'blur'
ousubmit
para reduzir chamadas excessivas. - Aplique debounce interno (com
timer
oudebounceTime
). - 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)