Como Implementar validadores de formulário personalizados no Angular
Validação de formulário é sempre um assunto quente não só no contexto do Angular, mas no contexto do Frontend em geral, pois é um requisito de negócio bem comum nas aplicações que implementam formulários no dia a dia.
A questão é, devo usar template-driven forms ou Reactive forms ?
Pois é, na verdade na maioria dos casos, tanto faz, sendo que é bastante fácil escrever validadores personalizados que funcionam perfeitamente tanto com template-driven forms quanto com Reactive forms, e é isso que vou te mostrar neste post.
Nesta prática vamos criar um validador de número de cartão de crédito personalizado. O validador deve garantir os seguintes requisitos:
- O número de cartão de crédito tenha 16 dígitos
- O número de cartão de crédito deve ser provido de uma empresa de cartão de crédito aceita (Amex, Visa ou Mastercard)
Para realizar essa validação, temos que implementar uma função de validação que tenha a seguinte assinatura:
interface ValidatorFn {
(c: AbstractControl): ValidationErrors | null
}
Essa é a assinatura do método usada tanto para template-driven forms quanto para Reactive forms. O parâmetro de entrada é um controle de formulário, e a saída é um objeto de erro ou nulo se o valor for válido.
Então, aqui está como esse método se pareceria:
static validateCcNumber(control: AbstractControl): ValidationErrors | null {
if (!control.value) {
return {
creditCard: '',
};
}
if (
!(
control.value.startsWith('37') ||
control.value.startsWith('4') ||
control.value.startsWith('5')
)
) {
// Retorna um erro se o cartão não é Amex, Visa ou Mastercard
return {
creditCard:
'O número do seu cartão de crédito não é de uma operadora de cartão de crédito compatível',
};
} else if (control.value.length !== 16) {
console.log(control.value);
// Retorna erro se não tiver 16 dígitos
return { creditCard: 'O número do cartão de crédito deve ter 16 dígitos' };
}
// Se não tiver erro, retorna nulo
return null;
}
Agora que nossa função validadora está definida, temos que torná-la aplicável a qualquer tipo de formulário.
Para template-driven forms, temos que torná-los uma diretiva que implemente uma interface específica chamada: Validator.
import { Directive } from '@angular/core';
import {
NG_VALIDATORS,
ValidationErrors,
Validator,
FormControl,
AbstractControl,
} from '@angular/forms';
@Directive({
standalone: true,
selector: '[validCreditCard]',
// Adicionamos nossa diretiva à lista de validadores existentes
providers: [
{ provide: NG_VALIDATORS, useExisting: CreditCardValidator, multi: true },
],
})
export class CreditCardValidator implements Validator {
// Este método é o exigido pela interface do Validador
validate(c: FormControl): ValidationErrors | null {
// Tudo o que fazemos é chamar o método estático
return CreditCardValidator.validateCcNumber(c);
}
// Este é o método estático que faz a validação real
static validateCcNumber(control: AbstractControl): ValidationErrors | null {
// Aqui vai o código de validação mencionado anteriormente
}
}
O código acima define nossa diretiva que implementa a interface Validator. Ele também registra essa diretiva como NG_VALIDATOR, que adiciona nosso validador à coleção de validadores existentes (como os validadores required ou pattern, por exemplo).
Agora posso definir um formulário e aplicar meu validador a um input de texto:
<form #form="ngForm" (ngSubmit)="displayData(form.value)">
<input type="text" name="cardnumber" ngModel validCreditCard />
</form>
E é isso! O formulário acima será inválido desde que o usuário não insira um número de cartão de crédito que atenda aos nossos critérios de validação.
Agora vamos adicionar algum tratamento de erros para renderizar a mensagem de erro correta de nossa função validadora:
<form #form="ngForm" (ngSubmit)="displayData(form.value)">
<input type="text" name="cardnumber" ngModel #cc="ngModel" validCreditCard />
<div *ngIf="cc.dirty && cc.errors?.creditCard" class="error">
{{cc.errors.creditCard}}
</div> <button type="submit" *ngIf="form.valid">Send info
</button>
</form>
E pronto, com algum estilo CSS adicionado, é isso que o código acima seria renderizado quando um número inválido for inserido:
Por que usar uma função de validação estática?
Esse é um truque para poder usar essa função de forma reativa. Como método estático, posso facilmente passar uma referência a ele da seguinte maneira, sem a necessidade de instanciar a class CreditCardValidator:
this.registerForm = this.formBuilder.group({
firstname: ['', Validators.required],
// ...
creditcardnumber: ['', CreditCardValidator.validateCcNumber]
});
Você pode encontrar o código completo desse exemplo no github.
Meu nome é Gustavo Bruno. Sou um Engenheiro de Software Sênior, especialista em frontend, com ênfase em Angular e React. Além de ser desenvolvedor, gosto de escrever sobre Frontend e desenvolvimento web para tentar ajudar a comunidade a criar aplicações mais robustas e escaláveis de acordo com cada contexto.
Se você gostou deste artigo, dá um clap👏 e compartilhe. Sua ajuda é sempre bem-vinda :).
Obrigado pela leitura!
Me acompanhe por aí! 😜
- Portfólio: gustavobruno.dev
- GitHub: @gustavobrunodev
- LinkedIn: @gustavobrunodev