Validação assíncrona - AsyncValidatorFn Angular
Como criar um formulário reativo (Reactive Forms) com validação assíncrona (AsyncValidatorFn) do Angular.

Antes de submeter os dados para o servidor, é importante garantir que todos os dados que o usuário informou estão de acordo com os requisitos do seu negócio, ou seja, os dados são consistentes e atendem às políticas do negócio. Por esta razão, a validação no client-side é importante e já foi alvo de estudo por aqui:
- https://consolelog.com.br/validacao-de-formulario-utilizando-o-reactiveformsmodule-do-angular-2/
- https://consolelog.com.br/validacao-formulario-ngmodel-angular/
- https://consolelog.com.br/como-criar-um-formulario-dinamico-angular/
- https://consolelog.com.br/validacao-componente-customizado-angular/
Neste texto será abordado como podemos realizar validações assíncronas. Por exemplo, o usuário está preenchendo um formulário de cadastro, um dos campos é o username. Ao preencher este campo a aplicação deve validar se o username está disponível para uso através da consulta à uma API.
Para realizar esta tarefa, iremos explorar um pouco do Reactive Forms e o AsyncValidatorFn
do Angular.
Estruturando o projeto
Versões utilizadas para criar o projeto:
Angular CLI: 14.2.3
Node: 16.16.0
Comandos para criar o projeto:
#Cria o diretório
mkdir consolelog-async-validator
#Entra no diretório
cd consolelog-async-validator
#Cria um novo projeto Angular
ng new projeto-async-validator
? Would you like to add Angular routing? No
? Which stylesheet format would you like to use? SCSS
Executando o projeto:
cd projeto-async-validator
npm run start

Configurando pacote de componentes PrimeNG
Com o projeto já configurado, poderíamos adicionar o @angular/material
para criar um formulário com um design mais interessante, mas para variar um pouco vamos trabalhar com os componentes do PrimeNG. Para isto vamos adicionar os seguintes pacotes:
npm install primeng
npm install primeicons
npm install primeflex
PrimeNG is a collection of rich UI components for Angular. All widgets are open source and free to use under MIT License. PrimeNG is developed by PrimeTek Informatics, a vendor with years of expertise in developing open source UI solutions. For project news and updates, please follow us on twitter and visit our blog.
Após a instalação é necessário adicionar no arquivo angular.json
algumas referências (linhas 29 - 32):
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"projeto-async-validator": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/projeto-async-validator",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": ["src/favicon.ico", "src/assets"],
"styles": [
"src/styles.scss",
"node_modules/primeicons/primeicons.css",
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
"node_modules/primeng/resources/primeng.min.css",
"node_modules/primeflex/primeflex.css"
],
"scripts": []
},
"configurations": {},
"defaultConfiguration": "production"
},
"serve": {},
"extract-i18n": {},
"test": {}
}
}
},
"cli": {}
}
Observação: no arquivo acima eu removi trechos que não são relevantes para esta configuração.
Criando um formulário com validação síncrona
Para criar o formulário vamos precisar importar alguns componentes do PrimeNG, ReactiveFormsModule
e vou aproveitar para já importar o HttpClientModule
que será usado mais à frente:
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { ButtonModule } from 'primeng/button';
import { InputTextModule } from 'primeng/inputtext';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [
BrowserModule,
ButtonModule,
HttpClientModule,
InputTextModule,
ReactiveFormsModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
O formulário terá 2 campos com as seguintes regras:
- username:
- campo obrigatório
- de 3 a 10 letras minúsculas - email:
- campo obrigatório
- deve ser um e-mail válido
O código ficará da seguinte forma:
import { Component } from '@angular/core';
import {
FormControl,
FormGroup,
Validators,
} from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
formGroup: FormGroup = new FormGroup({
username: new FormControl('', [
Validators.required,
Validators.pattern(/^[a-z]{3,10}$/),
]),
email: new FormControl('', [
Validators.required,
Validators.email,
]),
});
get username() {
return this.formGroup.get('username');
}
get email() {
return this.formGroup.get('email');
}
postar() {
if (this.formGroup.invalid) {
return;
}
console.log(this.formGroup.value);
}
}
<h1>Cadastro</h1>
<form
class="grid"
[formGroup]="formGroup"
(ngSubmit)="postar()">
<div class="col-12 md:col-6">
<!-- USERNAME -->
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-user"></i>
</span>
<input
autofocus
formControlName="username"
pInputText
placeholder="Cadastre um username"
type="text" />
</div>
<!-- USERNAME: MENSAGENS DE VALIDAÇÃO -->
<!--
Deixei o "username && username.errors"
na condição abaixo para evitar o uso
do safe navigation operator (?.) Fiz
isto porque acredito que o código ficou
mais legível, mas é só uma opinião pessoal!
-->
<div
class="p-error"
*ngIf="
username &&
username.errors &&
username.invalid &&
(username.dirty || username.touched)
">
<div *ngIf="username.errors['required']">
Username é obrigatório
</div>
<div *ngIf="username.errors['pattern']">
Este campo deve conter apenas letras
minúsculas de 3 a 10 caracteres
</div>
</div>
</div>
<!-- EMAIL -->
<div class="col-12 md:col-6">
<div class="p-inputgroup">
<span class="p-inputgroup-addon">
<i class="pi pi-at"></i>
</span>
<input
formControlName="email"
pInputText
placeholder="Informe seu e-mail"
type="email" />
</div>
<!-- EMAIL: MENSAGENS DE VALIDAÇÃO -->
<div
class="p-error"
*ngIf="
email &&
email.errors &&
email.invalid &&
(email.dirty || email.touched)
">
Por favor, informe um e-mail válido
</div>
</div>
<div class="col-12">
<button
[disabled]="formGroup.invalid"
label="Cadastrar"
pButton
type="submit"></button>
</div>
</form>
Executando o projeto é possível ver que o formulário está funcionando corretamente:

Agora vamos partir para a validação assíncrona do campo username
. Quando o usuário digitar um valor no campo username
, o frontend precisará consultar uma API para verificar se o valor informado está disponível para uso. Para simular este cenário vamos criar uma pequena API em Node.js.
API de validação
O código abaixo tem o único propósito de criar um ambiente em que possamos validar o username
, então questões como sanitização, semântica, http status code ou quaisquer outros pontos não relevantes para este contexto não foram considerados.
Basicamente o código irá pegar na URL (query string) o campo username
e irá verificar se o valor é igual a consolelog
. Este valor, consolelog
, será o único valor disponível para uso.
import { createServer } from "node:http";
import url from "node:url";
const server = createServer((req, res) => {
const query = url.parse(req.url, true).query;
res.write(
JSON.stringify({
disponivel: query.username === "consolelog",
})
);
res.end();
});
server.listen(3000, () =>
console.log("API pronta para uso")
);
{
"name": "api-validacao",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"keywords": [],
"type": "module",
"author": "",
"license": "ISC"
}
Utilizei o "type": "module"
para indicar que o formato dos módulos será tratado como ES Modules. Por isto utilizei o import
ao invés do require
no arquivo index.js
.
The"type"
field defines the module format that Node.js uses for all.js
files that have thatpackage.json
file as their nearest parent.
Files ending with.js
are loaded as ES modules when the nearest parentpackage.json
file contains a top-level field"type"
with a value of"module"
.
Link para mais detalhes.
Para testar a API usei o CURL no terminal, mas poderia ter usado um navegador qualquer:
$ npm run start
$ curl "localhost:3000?username=teste"
{"disponivel":false}
$ curl "localhost:3000?username=consolelog"
{"disponivel":true}
Agora vamos para a validação assíncrona no frontend.
Criando o AsyncValidator
Para realizar a validação assíncrona do campo username
vamos efetuar os seguintes passos:
- Criar uma service para se comunicar com a API:
UserService
- Criar uma classe com um método estático para retornar um
AsyncValidatorFn
- Alterar o
AppComponent
para incluir a validação assíncrona - Alterar o template do
AppComponent
para exibir uma mensagem ao usuário
1-UserService
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private http: HttpClient) {}
get(username: string) {
return this.http.get<{ disponivel: boolean }>(
`http://localhost:3000/?username=${username}`
);
}
}
2-AppValidator
Abaixo criamos um método estático, usernameJaExiste
, que recebe uma instância da UserService
como parâmetro e retorna um AsyncValidatorFn
. Deixei algumas explicações nos comentários ao longo do código abaixo:
import {
AbstractControl,
AsyncValidatorFn,
ValidationErrors,
} from '@angular/forms';
import { map, Observable } from 'rxjs';
import { UserService } from './user.service';
export class AppValidator {
static usernameJaExiste(
userService: UserService
): AsyncValidatorFn {
// *
// * Repare que este método retorna
// * um AsyncValidatorFn.
// *
// * No "return" abaixo temos uma função
// * com a assinatura do AsyncValidatorFn,
// * que é a seguinte:
// *
// * export declare interface AsyncValidatorFn {
// * (control: AbstractControl):
// * Promise<ValidationErrors | null> |
// * Observable<ValidationErrors | null>;
// * }
// *
return (
control: AbstractControl
): Observable<ValidationErrors | null> => {
// *
// * Conteúdo do método que é executado cada
// * vez que o usuário digita algo
// *
// * Pega o valor do FormControl:
const username = control.value;
// * Retorna um Observable resultante
// * da consulta à API:
return userService.get(username).pipe(
// * A API devolve um JSON no seguinte formato:
// * { "disponivel": true|false }
//
// * Abaixo é retornado um novo valor
// * dependendo do retorno da API.
map((response) => {
return response.disponivel
? // * Quando não há erros:
null
: // * Quando houver erros:
{ usernamejaexiste: username };
})
);
};
}
}
3-AppComponent
No app.component.ts
tiveram duas alterações:
- Injeção do
UserService
no construtor; - Inclusão do validador assíncrono no terceiro argumento do construtor do
FormControl
dousername
;
import { Component } from '@angular/core';
import {
FormControl,
FormGroup,
Validators,
} from '@angular/forms';
import { AppValidator } from './app.validator';
import { UserService } from './user.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
formGroup: FormGroup = new FormGroup({
username: new FormControl(
'',
[
Validators.required,
Validators.pattern(/^[a-z]{3,10}$/),
],
// Validações assíncronas:
[AppValidator.usernameJaExiste(this.userService)]
),
email: new FormControl('', [
Validators.required,
Validators.email,
]),
});
get username() {
return this.formGroup.get('username');
}
get email() {
return this.formGroup.get('email');
}
// Injeta a UserService para ser utilizada no
// validador assíncrono do username
constructor(private userService: UserService) {}
postar() {
if (this.formGroup.invalid) {
return;
}
console.log(this.formGroup.value);
}
}
4-Template do AppComponent
No template também tiveram duas alterações:
- Inclusão da mensagem de erro quando o
username
não está disponível para uso - Inclusão do
formGroup.pending
para desabilitar o botãoCadastrar
caso oFormGroup
ainda esteja processando alguma validação, que no nosso caso será a validação assíncrona dousername
:
<!-- ...(trecho ocultado)... -->
<!-- VALIDAÇÃO ASSÍNCRONA -->
<div *ngIf="username.errors['usernamejaexiste']">
<strong>{{
username.errors["usernamejaexiste"]
}}</strong>
não está disponível
</div>
<!-- ...(trecho ocultado)... -->
<div class="col-12">
<button
[disabled]="formGroup.invalid || formGroup.pending"
label="Cadastrar"
pButton
type="submit"></button>
</div>
<!-- ...(trecho ocultado)... -->
After asynchronous validation begins, the form control enters apending
state. Inspect the control'spending
property and use it to give visual feedback about the ongoing validation operation.
fonte: https://angular.io/guide/form-validation#creating-asynchronous-validators
Problema com CORS: no meu ambiente de testes o frontend está rodando na porta 4200 e a API de validação na porta 3000. Neste caso vamos ter problemas de Cross-Origin Resouce Sharing (CORS), então para efetuar um bypass
no seu navegador utilize uma das opções abaixo, lembrando que isto deve ser feito somente em ambiente de desenvolvimento:
- Chrome Windows: execute-o utilizando a seguinte opção:
chrome.exe" --disable-web-security --disable-gpu --user-data-dir=~/chromeTemp
. - Chrome macOS:
open -n -a /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
- Safari: vá até a aba Developer e selecione a opção
Disable Cross-Origin Restrictions
Executando o projeto temos o seguinte resultado:

No GIF acima é possível ver que o formulário está funcionando corretamente. Conforme o usuário preenche o campo username
, o navegador efetua uma requisição para validar a disponibilidade na API.
O grande problema deste modelo é a cada tecla pressionada no campo username
é disparada uma nova requisição. O ideal seria que essa requisição fosse disparada somente quando o usuário passar um determinado tempo sem digitar, por exemplo, disparar a requisição quando o intervalo entre as teclas pressionadas for maior que 500 milissegundos. Por sorte o RxJS oferece um operador chamado debounceTime
que faz este trabalho.
Aplicando o debounceTime no AsyncValidator
Abaixo há uma modificação no método usernameJaExiste
para incluir o debounceTime
. Deixei comentários ao longo do código para facilitar o entendimento:
import {
AbstractControl,
AsyncValidatorFn,
ValidationErrors,
} from '@angular/forms';
import {
BehaviorSubject,
debounceTime,
map,
Observable,
switchMap,
take,
} from 'rxjs';
import { UserService } from './user.service';
export class AppValidator {
static usernameJaExiste(
userService: UserService
): AsyncValidatorFn {
const subject = new BehaviorSubject<string>('');
const output = subject.asObservable().pipe(
// Só emite um valor quando o intervalo
// entre as teclas pressionadas for maior
// que 500ms
debounceTime(500),
// Retorna um observable que emite apenas
// o primeiro valor e então aplica o
// "completed" do Observable
take(1),
// "Troca" o valor emitido, que é o que o
// usuário digitou, por um novo observable
// resultante da consulta à API
switchMap((username: string) =>
userService.get(username).pipe(
// Como a API não retorna o "username",
// utilizamos o "map" para modificar a
// resposta obtida na consulta à API
map((apiResponse) => ({
...apiResponse,
username,
}))
)
),
map((response) => {
return response.disponivel
? // Se o usuário estiver disponível:
null
: // Caso o usuário não esteja disponível,
// emitimos o erro:
({
usernamejaexiste: response.username,
} as ValidationErrors);
})
);
return (
control: AbstractControl
): Observable<ValidationErrors | null> => {
const username = control.value;
// Sempre que o usuário digitar algo,
// passamos o valor para o "subject"
subject.next(username);
return output;
};
}
}

Após o ajuste no validador assíncrono, é possível perceber que as requisições à API de validação foram reduzidas e são feitas somente quando o intervalo entre a penúltima e a última tecla pressionada no campo username
for maior que 500ms.
Agora temos um formulário completo, com validações síncronas e assíncronas!
Considerações
Eventualmente sua fonte de validação será assíncrona, como vimos no texto acima, consequentemente o AsyncValidatorFn
será um ótimo recurso para se usar.
Um outro ponto relevante que podemos destacar, é o aumento da complexidade do código quando aplicamos o debounceTime
. Por outro lado, esta alteração entregou um belo ganho em performance ao reduzir a quantidade de chamadas ao backend.
Links interessantes: