Validação de formulário com ngModel - Angular

Em um outro post, Validação de formulário utilizando o ReactiveFormsModule do Angular 2+, foi apresentado como podemos construir uma validação de formulário utilizando alguns recursos como FormGroup, FormControl, FormBuilder, entre outros. Neste texto vamos abordar uma outra forma de construir validação de formulários com o uso da diretiva ngModel.

Introdução

Considere o template abaixo:

<form (ngSubmit)="postar()">
  <div>
    <input type="text"
           name="nome"
           placeholder="Nome..." />
  </div>
  <div>
    <button type="submit">
      Enviar
    </button>
  </div>
</form>

Quando o botão Enviar for clicado, o formulário sofrerá um post e o método postar() será chamado.

Neste exemplo não estamos enviando os dados do formulário para o método postar() e também não estamos validando nenhuma informação.

Para facilitar o trabalho e podermos pegar os dados dos campos do formulário e também fazer uma validação, vamos criar uma instância do FormControl e vinculá-lo ao input através do uso da diretiva ngModel.

Diretiva ngModel

O primeiro passo é garantir que o módulo do seu componente tenha importado o FormsModule. Neste artigo os códigos de exemplo serão incluídos nos componentes Exemplo1Component e Exemplo2Component que estão declarados no AppModule. Logo, temos que importar o FormsModule no AppModule conforme o exemplo abaixo:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import {
  Exemplo1Component
} from './exemplos/exemplo1.component';
import {
  Exemplo2Component
} from './exemplos/exemplo2.component';

/* ... (código não relevante) ... */

@NgModule({
  imports: [
    AppRoutingModule,
    BrowserModule,
    FormsModule,
  ],
  declarations: [
    AppComponent,
    Exemplo1Component,
    Exemplo2Component,
    /* ... (código não relevante) ... */
  ],
  bootstrap: [
    AppComponent
  ]
})
export class AppModule { }
app.module.ts

Vamos declarar um método dentro da classe para sabermos que o formulário foi postados e na sequência abordaremos seu template:

import { Component } from '@angular/core';

@Component({
    templateUrl: './exemplo1.component.html' 
})
export class Exemplo1Component {
  postar() {
    console.log('Post efetuado!');
  }
}
exemplo1.component.ts

Agora voltamos a atenção para o template. Vamos adicionar a diretiva ngModel no input para criar um FormControl:

<form (ngSubmit)="postar()">
  <div>
    <input ngModel
           name="nome"
           placeholder="Nome..."
           type="text" />
  </div>
  <div>
    <button type="submit">
      Enviar
    </button>
  </div>
</form>
exemplo1.component.html

Para pegarmos a referência desta diretiva, ngModel, podemos escrever algo do tipo: #minhaVariavel="ngModel".


Lembre-se de que durante a criação de uma diretiva podemos preencher a propriedade exportAs para definir um nome, um nome que possa ser utilizado no template para pegarmos a instância da diretiva. No caso do ngModel é "ngModel".

Fonte: https://angular.io/api/core/Directive#exportas

To inspect the properties of the associated FormControl (like the validity state), export the directive into a local template variable using ngModel as the key (ex: #myVar="ngModel"). You can then access the control using the directive's control property. However, the most commonly used properties (like valid and dirty) also exist on the control for direct access. See a full list of properties directly available in AbstractControlDirective.

https://angular.io/api/forms/NgModel#description

Seguindo a lógica acima:

<form (ngSubmit)="postar()">
  <div>
    <input ngModel
           #formNome="ngModel"
           name="nome"
           placeholder="Nome..."
           type="text" />
  </div>
  <div>
    <button type="submit">
      Enviar
    </button>
  </div>
</form>
exemplo1.component.html

No template acima criamos um FormControl vinculado ao nosso único input e passamos sua referência para a variável formNome. Então vamos testar se realmente formNome: FormControl está configurado corretamente adicionando alguns trechos a mais:

<form (ngSubmit)="postar()">
  <div>
    <input ngModel
           #formNome="ngModel"
           name="nome"
           placeholder="Nome..."
           type="text" />
  </div>
  <div>
    <button type="submit">
      Enviar
    </button>
  </div>
</form>

<!-- Trecho para entendimento -->
<hr>
<div>
  formNome.value:
  {{formNome.value}}
</div>
<div>
  formNome.valid:
  {{formNome.valid}}
</div>
<div>
  <!-- Para utilizar o "| json"
       importe o CommonModule -->
  formNome.errors:
  {{formNome.errors | json}}
</div>
exemplo1.component.html

Podemos observar no resultado abaixo que conseguimos acessar o valor do input bem como outras informações como valid e errors:

Exemplo da execução do código acima

Iniciando a validação do input

Agora vamos evoluir um pouco mais e adicionar algumas validações e melhorias:

  • O campo nome é obrigatório
  • O campo nome deve ter pelo menos 3 caracteres
  • O botão Enviar deve ficar desabilitado caso o campo nome tenha um valor inválido

As alterações são efetuadas direto no template:

<form (ngSubmit)="postar()">
  <div>
    <input ngModel
            #formNome="ngModel"
            required
            minlength="3"
            name="nome"
            placeholder="Nome..."
            type="text" />
  </div>
  <div>
    <!--
         O botão é desabilitado
         conforme o estado de
         validação do formNome:
     -->
    <button [disabled]="formNome.invalid"
            type="submit">
      Enviar
    </button>
  </div>
</form>
<hr>
<div>
  formNome.value:
  {{formNome.value}}
</div>
<div>
  formNome.valid:
  {{formNome.valid}}
</div>
<div>
  formNome.errors:
  {{formNome.errors | json}}
</div>
exemplo1.component.html

Veja no resultado abaixo que toda vez que digitamos algo no input:

  • o formNome.value muda conforme o valor do input;
  • o formNome.valid ou formNome.invalid muda conforme o valor do input fica válido/inválido. Veja que o botão Enviar as vezes fica verde (habilitado) e as vezes cinza (desabilitado)
  • o formNome.errors muda conforme os erros de validação mudam. Quando não há erros, ou seja, o valor é válido, o formNome.errors apresenta null
Formuário com validações no campo nome

Para finalizar precisamos:

  1. pegar o valor deste input e passar para o método postar
  2. adicionar algumas mensagens de validação para ajudar o usuário

Alterando o método postar para receber um parâmetro (nome):

import { Component } from '@angular/core';

@Component({
    templateUrl: './exemplo1.component.html' 
})
export class Exemplo1Component {
    postar(nome: string) {
    console.log('Post efetuado:', nome);
  }
}
exemplo1.component.ts

Alterando o template para incluir as mensagens de validação e também passar o valor do input no método postar:

<form (ngSubmit)="postar(formNome.value)">
  <div>
    <input ngModel
           #formNome="ngModel"
           required
           minlength="3"
           name="nome"
           placeholder="Nome..."
           type="text" />
  </div>

  <!--
       Mensagens para o
       usuário visualizar
       o que está errado
       na validação
   -->
  <div *ngIf="formNome.errors?.required"
       class="erro-detalhes">
    Campo obrigatório
  </div>
  <div *ngIf="formNome.errors?.minlength"
       class="erro-detalhes">
    Informe ao menos
    {{formNome.errors.minlength.requiredLength}}
    caracteres
  </div>

  <div>
    <button [disabled]="formNome.invalid" 
            type="submit">
      Enviar
    </button>
  </div> 
</form>
exemplo1.component.html

Formulário HTML gerado a partir do código acima

Validação de múltiplos campos

Vamos adicionar mais um campo: sobrenome. Basicamente vamos duplicar o conteúdo do <input name="nome" ....

Na classe vamos adicionar mais um parâmetro no método postar para receber o sobrenome:

import { Component } from '@angular/core';

@Component({
    templateUrl: './exemplo2.component.html' 
})
export class Exemplo2Component {
    postar(nome: string,
           sobrenome: string) {

    console.log('Valor formulário',
                nome,
                sobrenome);
  }
} 
exemplo2.component.ts

No template duplicamos toda a lógica envolvendo o input nome e criamos o input sobrenome:

<form (ngSubmit)="postar(formNome.value, formSobrenome.value)">
  <!-- Campo Nome -->
  <div>
    <input ngModel
            #formNome="ngModel"
            required
            minlength="3"
            name="nome"
            placeholder="Nome..."
            type="text" />
  </div>
  <div *ngIf="formNome.errors?.required"
      class="erro-detalhes">
    Campo obrigatório
  </div>
  <div *ngIf="formNome.errors?.minlength"
      class="erro-detalhes">
    Informe ao menos
    {{formNome.errors.minlength.requiredLength}}
    caracteres
  </div>

  <!-- Campo Sobrenome
       Para ficar levemente diferente 
       o minlength foi alterado para 2 -->
  <div>
    <input ngModel
          #formSobrenome="ngModel"
          required
          minlength="2"
          name="sobrenome"
          placeholder="Sobrenome..."
          type="text" />
  </div>
  <div *ngIf="formSobrenome.errors?.required"
      class="erro-detalhes">
    Campo obrigatório
  </div>
  <div *ngIf="formSobrenome.errors?.minlength"
      class="erro-detalhes">
    Informe ao menos
    {{formSobrenome.errors.minlength.requiredLength}}
    caracteres
  </div>

  <div>
    <button [disabled]="formNome.invalid || formSobrenome.invalid" 
            type="submit">
      Enviar
    </button>
  </div>
</form>
exemplo2.component.html

Veja que apenas duplicamos os inputs ajustando os respectivos nomes. Aqui temos alguns pontos importantes:

  1. Agora o botão Enviar analisa dois valores ao invés de um para saber se deve ficar desabilitado ou habilitado
  2. No (ngSubmit) do formulário estamos passando dois campos ao invés de 1

Imagine que este formulário poderá crescer contento diversos campos. Ficará um pouco inviável seguir nesta linha. Para resolver este problema vamos utilizar um FormGroup  no <form> através da sintaxe #minhaVariavel="ngForm".

Vamos alterar o parâmetro do método postar. Agora receberá um objeto contendo um nome e um sobrenome:

import { Component } from '@angular/core';

@Component({
    templateUrl: './exemplo2.component.html' 
})
export class Exemplo2Component {
    postar(dados: { nome: string, sobrenome: string }) {
    console.log('Valor formulário', dados);
  }
} 
exemplo2.component.ts

No template as alterações são pequenas:

  1. Incluir o FormGroup no <form>
  2. Ajustar a chamada do método postar
  3. Ajustar o [disabled] do botão Enviar
<form #meuForm="ngForm"
      (ngSubmit)="postar(meuForm.value)">
  
  <!-- ...(código ocultado)... -->

  <div>
    <button [disabled]="meuForm.invalid" 
            type="submit">
      Enviar
    </button>
  </div>
</form>
exemplo2.component.html

Resultado:

Formulário com validação em dois campos

Criando estilos baseados no estado

Agora que o formulário está funcionando corretamente, vamos focar no estilo visual.

Repare na imagem acima que há uma barra à esquerda do formulário que muda de cor conforme seu estado (válido/inválido). Conseguimos chegar neste efeito utilizando algumas classes CSS (tabela abaixo) que o Angular injeta tanto no <form> quando nos inputs que recebem o ngModel.

State Class if true Class if false
The control has been visited ng-touched ng-untouched
The control's value has changed ng-dirty ng-pristine
The control's value is valid ng-valid ng-invalid

fonte

Então para saber se o formulário está válido ou inválido podemos utilizar ng-valid e ng-invalid conforme o CSS abaixo:

form.ng-valid {
    border-left: 4px solid #4CAF50;
}

form.ng-invalid {
    border-left: 4px solid #f44336;
}

Veja na imagem abaixo para entender melhor:

Angular injetando as classes ng-valid e ng-invalid no formulário e inputs

Considerações

Este recurso é extremamente útil para criar validações de forma bem rápida e eficiente. Não cheguei a abordar o two way data binding ([(ngModel)]="variavel"), mas caso tenha interesse em saber mais sobre o assunto: https://angular.io/guide/two-way-binding.

Caso você não esteja tão familiarizado com validações utilizando Angular, sugiro dar uma lida nestes posts:

Validacao Formulario - Template Driven - StackBlitz
Exemplo de validações com Angular usando Template Driven
Link com a implementação dos exemplos

Links: