Validação de formulário utilizando o ReactiveFormsModule do Angular 2+

Este artigo tem como objetivo mostrar passo a passo como criar uma validação de formulário, exibir mensagens de erro e habilitar/desabilitar o botão de Postar de acordo com a validade do formulário.

Validação de formulário utilizando o ReactiveFormsModule do Angular 2+

Começando a validação do formulário HTML da forma "tradicional"

Para quem está començando com Angular é natural pensar nos eventos como o keyup para preencher uma variável e depois efetuar a validação, por exemplo:

<form>
    <input type="text" 
           placeholder="Nome"
           (keyup)="nome = $event.target.value" />
    
    <input type="password" 
           placeholder="Senha"
           (keyup)="senha = $event.target.value" />
    
    <!-- ... -->
    
    <button (click)="postar()">Postar</button>
    <button>Reset</button>
</form>

…e na classe do componente algo do tipo:

@Component({ /* ... */})
export class MeuComponent {
    nome: string;
    senha: string;

    // ...

    postar() {
        if(!nome || !senha) {
            /* ... */
            console.log('Formulário inválido');
            return;
        }

        console.log('Formulário válido');
   }
}

Com toda certeza esta lógica funcionaria mas daria um certo trabalho conforme avançamos no desenvolvimento desta validação. A final de contas, ainda faltam validar tamanho mínimo/máximo, e-mail, desabilitar o botão Postar caso o formulário esteja inválido, exibir mensagens de erro, enfim.

FormGroup & FormControl

No Angular 2+ há duas classes que nos ajudarão a trabalhar com a validação de formulários. Essas classes são FormGroup e FormControl.

Antes de entrar nos detalhes sobre cada uma delas vamos ver na prática como ficaria uma simples validação, se atente aos comentário no código:

import { Component } from "@angular/core";
import { FormBuilder, FormGroup, Validators } from "@angular/forms";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  meuFormGroup: FormGroup;

  constructor(private formBuilder: FormBuilder) {
    // **************************************************
    // Abaixo utilizamos o formBuilder para construir
    // vários FormControls que fazem parte do formulário.
    // Cada FormControl valida um input do formulário
    // **************************************************
    this.meuFormGroup = this.formBuilder.group({
        // *********************************************
        // O valor padrão deste formControl será Marcelo
        // e os demais vazio
        // *********************************************
        nome: ['Marcelo', Validators.required],
        senha: ['', Validators.required],
        email: ['', [
            Validators.required,
            Validators.email
        ]]
    });
  }
    
  postar() {
    if (!this.meuFormGroup.valid) {
      console.log("Formulário inválido");
      return;
    }
    console.log("Formulário válido", this.meuFormGroup.value);
  }
}

Na classe acima declaramos o meuFormGroup que agrupa 3 campos:

  • nome
  • senha
  • email

O próximo passo é vincular o meuFormGroup ao <form> do template, para isto utilizamos o atributo [formGroup]="meuFormGroup" no formulário e nos inputs utilizamos formControlName="<nome do formControl>" coforme o seguinte código:

<form [formGroup]="meuFormGroup">
    <input type="text"
           placeholder="Nome"
           formControlName="nome" />

    <input type="email"
           placeholder="Email"
           formControlName="email" />
	
    <input type="password"
           placeholder="Senha"
           formControlName="senha" />

    <button (click)="postar()">
        Postar
    </button>
</form>

Após vincular o FormGroup e seus FormControl ao template fica fácil saber se o formulário é válido ou não, basta acessarmos a propriedade validconforme o método postar() do AppComponent mostra. Veja o resultado na imagem abaixo:

Resultado da implementação da validação de formulário
Resultado da implementação da validação de formulário

Repare que os dois primeiros cliques mostram no console que o formulário está inválido. Após preencher todos os campos os valores do formulário são printados no console.

Pegando os dados do formulário

Bom, já sabemos como verificar se o formulário de uma forma geral está válido ou não, mas como fazemos para pegar os valores dos inputs?

Para pegar os dados do formulário basta utilizar a propriedade value do FormGroup e será retornado um objeto com os valores, veja o exemplo abaixo:

console.log(this.meuFormGroup.value);

// Retorno:
//  {
//    email: "[email protected]",
//    lembrar: "",
//    nome: "Marcelo",
//    senha: "123"
//  }

Habilitando e desabilitando o botão de Post de acordo com a validação do formulário

Como já sabemos identificar se o formulário está válido ou não, fica mais fácil de desabilitar/habilitar o botão Postar de acordo com a validação.

Para desabilitar o botão podemos utilizar a propriedade disable do input. Lembrando que oFormGroup tem uma propriedade chamada valid: booleanque retorna true quando todos os componentes habilitados estão válidos, então podemos escrever o seguinte :

<form [formGroup]="meuFormGroup">
    <input type="text"
           placeholder="Nome"
           formControlName="nome" />

    <input type="email"
           placeholder="Email"
           formControlName="email" />
	
    <input type="password"
           placeholder="Senha"
           formControlName="senha" />

    <button (click)="postar()"
            [disabled]="!meuFormGroup.valid">
        Postar
    </button>
</form>
O botão do formulário é habilitado ou desabilitado conforme a validade do formulário
Desabilitando e habilitando o botão conforme a validade do formulário

Exibindo mensagens de erro

Para exibir as mensagens de erro, podemos no próprio template acessar o respectivo FormControl e verificar se ele é válido/inválido. No caso de ser inválido podemos apresentar uma mensagem de erro da seguinte forma:

<form [formGroup]="meuFormGroup">
    <input type="text"
           placeholder="Nome"
           formControlName="nome" />
    <span *ngIf="meuFormGroup.controls.nome.errors">
       Campo obrigatório
    </span>

    <input type="email"
           placeholder="Email"
           formControlName="email" />
    <span *ngIf="meuFormGroup.controls.email.errors">
       Campo obrigatório
    </span>

    <input type="password"
           placeholder="Senha"
           formControlName="senha" />
    <span *ngIf="meuFormGroup.controls.senha.errors">
       Campo obrigatório
    </span>

    <button (click)="postar()"
            [disabled]="!meuFormGroup.valid">
        Postar
    </button>
</form>

Observação: para utilizar o *ngIf é necessário importar o módulo CommonModule.

Resultado:

Exibindo mensagens de erro para cada input do formulário
Exibindo as mensagens de erro em cada campo do formulário

Refinando as mensagens de erro

Repare (classse AppComponent) que o campo email tem dois Validators, required e email. O primeiro valida a obrigatoriedade do campo e o segundo valida se o que o usuário digitou realmente é um e-mail:

// ...
email: ["", [Validators.required, Validators.email]],
// ...

Para identificar qual Validator emitiu o erro podemos utilizar a seguinte lógica no template:

<input type="email"
       placeholder="Email"
       formControlName="email" />
<span *ngIf="meuFormGroup.controls.email.errors?.required">
    Campo obrigatório
</span>
<span *ngIf="meuFormGroup.controls.email.errors?.email">
    E-mail inválido
</span>

Resultado:

O campo e-mail apresenta o erro de forma específica, indicando se o usuário não digitou um e-mail ou se o e-mail é inválido
Apresentando o erro específico para o usuário

Como exibir as mensagens de erro após o onBlur

Veja que até agora as mensagens de erro aparecem na tela logo após seu carregamento. O desejável seria que as mensagens de erro só fossem exibidas quando o usuário interagisse com o componente, por exemplo no evento onBlur, ou seja, toda vez que o usuário retirasse o foco do componente (evento onBlur) exibiríamos a mensagem caso seu valor fosse inválido.

Felizmente existe uma propriedade do FormControlchamadatouched. Esta propriedade é do tipo booleane seu valor inicial é false. Sempre que o input dispara o eventoonBlur a propriedade touched do respectivo FormControl recebe o valor true. Então podemos aproveitá-la para exibir as mensagens de erro apenas quando o usuário interagir com o componente:

<form [formGroup]="meuFormGroup">
    <input type="text"
           placeholder="Nome"
           formControlName="nome" />
    <ng-container *ngIf="meuFormGroup.controls.nome.touched">
        <span *ngIf="meuFormGroup.controls.nome.errors">
            Campo obrigatório
        </span>
    </ng-container>

    <input type="email"
           placeholder="Email"
           formControlName="email" />
    <ng-container *ngIf="meuFormGroup.controls.email.touched">
        <span *ngIf="meuFormGroup.controls.email.errors?.required">
            Campo obrigatório
        </span>
        <span *ngIf="meuFormGroup.controls.email.errors?.email">
            E-mail inválido
        </span>
    </ng-container>
	
    <input type="password"
           placeholder="Senha"
           formControlName="senha" />
    <ng-container *ngIf="meuFormGroup.controls.senha.touched">
        <span *ngIf="meuFormGroup.controls.senha.errors">
            Campo obrigatório
        </span>
    </ng-container>

    <button (click)="postar()"
            [disabled]="!meuFormGroup.valid">
        Postar
    </button>
</form>

Resultado:

Mostra as mensagens de erro somente após o onblur de cada componente
Mostrando as mensagens de erro após o onblur

Considerações

A utilização das classesFormControl eFormGroup ajudam bastante a construir formulários mais interativos com muito menos código.

Veja o exemplo completo em funcionamento com todo o código fonte neste link:

https://validando-formulario-angular-reactiveformsmodule.stackblitz.io

Observação

Se você está tentando utilizar o formControlName em componentes customizados (<meu-componente formControlName="...">...), será necessário implementar a classe ControlValueAccessor, mas isto é assunto para outro artigo :)