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:
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:
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):
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:
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:
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.
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 that package.json file as their nearest parent.
Files ending with .js are loaded as ES modules when the nearest parent package.json file contains a top-level field "type" with a value of "module".
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
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:
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 do username;
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ão Cadastrar caso o FormGroup ainda esteja processando alguma validação, que no nosso caso será a validação assíncrona do username:
After asynchronous validation begins, the form control enters a pending state. Inspect the control's pendingproperty and use it to give visual feedback about the ongoing validation operation.
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:
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.