Fomulários

A partir da versão 2, o Angular possibilita duas formas de validação de formulários: Template Driven e Model Driven/Reative Forms. Nesse artigo veremos as diferenças entre os dois, vantagens e desvantagens de cada um.

A versão 1 do Angular, hoje referenciada como AngullarJS trata os formulários com two-way data binding por padrão através da diretiva ng-model. A versão equivalente dessa diretiva em Angular (2 e adiante) também se chama ngModel - Angular usa lowerCamelCase para diretivas ao invés de kebab-case que é utilizado pelo AngularJS.

Template Driven Form

Confira aqui um exemplo para form do tipo Template Driven: Plunker: https://plnkr.co/edit/nn5qE6SCD0H247aGIP0P?p=preview

Vamos ver o que deve ser feito passo a passo. Primeiramente o módulo FormsModule deve ser importado no módulo principal da sua aplicação:

...
import {FormsModule} from "@angular/forms";
...
@NgModule({
    ...
    imports: [..., FormsModule],
    ...
})
export class AppModule {}
...

No exemplo temos um formulário construído da forma Template Driven:

<section class="sample-app-content">
    <h1>Template-driven Form Example:</h1>
    <form #f="ngForm" (ngSubmit)="onSubmitTemplateBased(f.value)">
        <p>
            <label>First Name:</label>
            <input type="text"
                name="nome"
                [(ngModel)]="user.firstName" required>
        </p>
        <p>
            <label>Password:</label>
            <input type="password"
                name="password"
                [(ngModel)]="user.password" required>
        </p>
        <p>
            <button type="submit" [disabled]="!f.valid">Submit</button>
        </p>
    </form>
</section>

Nesta parte

<form #f="ngForm" ...>

estamos associando ao nosso formulário #f uma instância de ngForm. E aqui

(ngSubmit)="onSubmitTemplateBased(f.value)

estamos associando o handler do botão de confirmação a uma função que está definida no componente.

Note que o botão Submit só ativa se o formulário for válido.

[disabled]="!f.valid"

Até agora as únicas pré condições para a validação são os atributos required nos campos firstName e password.

Data Binding

Note o uso da diretiva ngModel dentro da notação [(...)] (chamada de caixa de bananas). O parêntesis entre colchetes significa a ligação entre o conteúdo das variáveis é bidirecional.

Quando ngModel é utilizada o atributo name deve ser incluído na input porque o Angular usa esse atributo como identificador para controlar as mudanças de valor e estado.

Você pode experimentar a inicialização da variável através do uso de [(ngModel)] pelo exemplo do plunker setando um valor padrão para user.firstName

user: Object = {
  firstName = 'Nome' 
};

no componente e vendo que este valor reflete no campo correspondente.

E se precisarmos apenas da inicialização dos campos, sem two-way data binding? Nesse caso a sintaxe seria

[ngModel]="user.firstName"

ao invés de

[(ngModel)]="user.firstName"

Mudanças em um atributo atrelado a [ngModel] podem ser monitoradas como o evento (ngModelChange). Por exemplo, em:

<input [ngModel]="username" (ngModelChange)="username = $event">

o valor inicial de username está sendo atribuído a tag input com

[ngModel]="username"

e as mudanças estão sendo atualizadas com a expressão

"username = $event"

que por sua vez foi atribuída ao evento

(ngModelChange)

Se por algum motivo a entrada precisar ser processada durante a mudança, o desenvolvedor pode ainda atribuir uma função a (ngModelChange), assim:

(ngModelChange)="onChange($event)"

no template, e, no componente:

onChange($event) {
  console.log($event);
}

Para verificar a saída console pressione F12 no Chrome ou Firefox e selecione a aba console.

Plunker: https://plnkr.co/edit/XqUTB25DrAQWj5dFvV96?p=preview

Ainda, do exemplo anterior, em dual data binding ou não, os valores dos campos de um formulário são armazenados na variável value de ngForm. Isto é, como passamos esse dado como parâmetro da função atribuída a (ngSubmit)

<form #f="ngForm" (ngSubmit)="onSubmitTemplateBased(f.value)">

vamos receber um objeto json com os campos e seus respectivos valores no evento (ngSubmit):

onSubmitTemplateBased(value: any) {
    console.log(value);
}

Isso vai imprimir, por exemplo:

Object {firstName: "Fulano", password: "serpro"}

Na medida em que os formulário crescem em complexidade é conveniente agrupar os campos de acordo com sua semântica. Vamos incrementar o exemplo com dados de login, dados pessoais e endereço, introduzindo a diretiva ngModelGroup:

<form #f="ngForm" (ngSubmit)="onSubmitTemplateBased(f.value)">
  <fieldset ngModelGroup="login">
    <p>
      <label>Login:</label>
      <input type="text"  [(ngModel)]="user.login" name="login" required>
    </p>
    <p>
      <label>Password:</label>
      <input type="password"  [(ngModel)]="user.password" name="password" required>
    </p>
  </fieldset>
  <fieldset ngModelGroup="userData">
    <p>
      <label>First Name:</label>
      <input type="text"  [(ngModel)]="user.firstName" name="firstName" required>
    </p>
    <p>
      <label>Last Name:</label>
      <input type="text"  [(ngModel)]="user.lastName" name="lastName" required>
    </p>
  </fieldset>
  <fieldset ngModelGroup="address">
    <p>
      <label>Street:</label>
      <input type="text"  [(ngModel)]="user.street" name="street" required>
    </p>
    <p>
      <label>City:</label>
      <input type="text"  [(ngModel)]="user.city" name="city" required>
    </p>
  </fieldset>
  <p>
    <button type="submit" [disabled]="!f.valid">Submit</button>
  </p>
</form>

Então, se imprimirmos o conteúdo de value do formulário durante a submissão, teremos, por exemplo:

Object {
  address: Object {
    city: "Bananal",
    street: "XV de Novembro"
  }
  login: Object {
    login:"fulano",
    password: "serpro"
  }
  userData: Object {
    firstName: "Fulano",
    lastName: "do Serpro"
  }
}

Plunker: https://plnkr.co/edit/1FRZU1q0IW6KOmsxLedU?p=preview

Validação

Os validadores que o Angular suporta por padrão são:

  • required - Valor não vazio.
  • minlength - Número mínimo de caracteres.
  • maxlength - Número máximo de caracteres.
  • pattern - Valor do campo tem que atender a expressão regular.

Vejamos um exemplo:

<form #f="ngForm" (ngSubmit)="onSubmit(f.value)">
<p><label>Nome: </label><input type="text" name="nome" ngModel required></p>
<p><label>Rua: </label><input type="text" name="rua" ngModel minlength="3"></p>
<p><label>Cidade: </label><input type="text" name="cidade" ngModel maxlength="30"></p>
<p><label>CEP: </label><input type="text" name="cep" ngModel pattern="[0-9]{8}"></p>
<p><button type="submit">Submit</button></p>
</form>

Note a presença da diretiva ngModel sem atribuir a uma propriedade no componente. Isso serve para registrar a input no controlador de formulário do Angular. Como já foi dito, o uso da diretiva ngModel requer o uso do atributo name em uma input.

Plunker: https://plnkr.co/edit/nYQnDBiWYxPu3WxrNBEp?p=preview

Para controlar o estado das entradas html o Angular faz uso dos seguintes controles:

  • touched ou untouched
  • valid ou invalid
  • pristine ou dirty

Uma maneira didática de entender a mudança de estado de uma entrada é imprimir a classe que o Angular atribui a essa entrada. Para isso vamos usar chaves duplas, que é um bind do componente para a visão:

<input type="text" name="nome" #nameState ngModel required>
{{nameState.className}}

Brinque com as entradas nesse exemplo

PLunker: https://plnkr.co/edit/9tNqibCd5IwUjpmYwZjZ?p=preview

e observe que:

  • touched: verdadeiro se o elemento já perdeu foco.
  • untouched: verdadeiro se o elemento ainda não perdeu foco.
  • valid: verdadeiro se o elemento passa na validação.
  • invalid: verdadeiro se o elemento não passa na validação.
  • pristine: verdadeiro se o usuário ainda não interagiu com o elemento.
  • dirty: verdadeiro se o usuário já interagiu com o elemento.

Para exibir mensagens customizadas em caso de erros durante a entrada de dados, além do padrão do navegador, podemos fazer uso da diretiva hidden com a seguinte sintaxe

[hidden]="expressão"

e usar esses controles de estado para incluir ou não tags de aviso no template html.

<small [hidden]="false">
Nome é obrigatório.
</small>

Então agora precisamos de uma forma de acessar os controles citados para cada elemento que precisa de validação. Para tanto vamos criar uma variável local de template e setá-la para ngModel.

<input type="text" name="nome" #nameState ngModel #vltNome="ngModel" required>
{{nameState.className}}

E com esse acesso podemos agora colocar as condições para exibição da mensagem:

<small [hidden]="vltNome.valid || (vltNome.pristine && !submitted)">
Nome é obrigatório.
</small>

Então, nesse exemplo, se o elemento nome for inválido e se o usuário interagiu com o elemento e o formulário não tiver sido submetido, a mensagem de aviso aparece.

É possível resetar o formulário com a função resetForm(). No exemplo, vamos enviar o formulário como parâmetro na função onSubmit:

<form #f="ngForm" (ngSubmit)="onSubmit(f)">

e resetar os valores e controles ao submeter:

onSubmit(form) {
    form.resetForm();
}

Note então que os controles retornam para ng-untouched, ng-pristine e ng-invalid em todos os campos.

Plunker: https://plnkr.co/edit/GBuoRe?p=preview

Model Driven Form ou Reactive Form

Um formulário orientado a template nos provê de uma forma de criar formulários com uso reduzido de código. Já os formulários orientados ao modelo nos permitem testar um formulário sem precisar de testes do tipo fim-a-fim (teste que depende da interação com o template). Isso porque os elementos de um formulário orientado ao modelo são propriedades definidas dentro dos componentes.

Então vamos reiniciar nossos estudos com um formulário zerado:

<form>
  <label>Primeiro Nome: </label>
  <input type="text">

  <label>Sobrenome: </label>
  <input type="text">

  <label>Rua: </label>
  <input type="text">

  <label>Cidade: </label>
  <input type="text">

  <label>CEP: </label>
  <input type="text">

  <button type="submit">Submit</button>
</form>

Importar FormGroup e FormControl:

import { FormGroup, FormControl } from '@angular/forms';

Criar o modelo que representa nosso formulário no componenete e registrar controles para cada campo do formulário:

registerForm = new FormGroup({
  primeironome: new FormControl(),
  sobrenome: new FormControl(),
  endereco: new FormGroup({
    rua: new FormControl(),
    cidade: new FormControl(),
    cep: new FormControl()
  })
});

Assim criamos uma propriedade no componente do tipo FormGroup - registerForm - que representa nosso formulário. Para cada campo, criamos um FormControl. Esta classe monitora o valor e validade de um campo individualmente. Note que os FormGroup podem ser aninhados, agrupando classoes FormControl e FormGroup. No exemplo, criamos um grupo para representar os dados de endereço.

O próximo passo é associar os controles do formulário ao template. Para tanto precisamos usar a diretiva formGroup que recebe uma expressão e então avalia uma instância do FormGroup. Então vamos importar o módulo ReactiveFormsModule no nosso módulo principal.

import {NgModule} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import {ReactiveFormsModule} from "@angular/forms"
import {AppComponent} from './app.component'
import {TemplateDrivenForm} from './template-driven-form'

@NgModule({
  imports: [ BrowserModule, ReactiveFormsModule ],
  declarations: [ AppComponent, TemplateDrivenForm ],
  bootstrap: [ AppComponent ]
})
export class AppModule {}

Agora podemos linkar o template ao componente:

<form [formGroup]="registerForm">
  ...
</form>

E associar cada campo ao ser respectivo controle. Note que a diretiva ngModel e o atributo name não são mais necessários.

<form [formGroup]="registerForm">
  <label>Primeiro Nome: </label>
  <input type="text" formControlName="primeironome">

  <label>Sobrenome: </label>
  <input type="text" formControlName="sobrenome">

  <label>Rua: </label>
  <input type="text" formControlName="rua">

  <label>Cidade: </label>
  <input type="text" formControlName="cidade">

  <label>CEP: </label>
  <input type="text" formControlName="cep">

  <button type="submit">Submit</button>
</form>

E como agrupamos os dados de endereço, vamos envolver os campos do formulário em um fieldset e associá-lo ao respectivo FormGroup através da diretiva formGroupName:

...
<fieldset formGroupName="endereco">
  <label>Rua: </label>
  <input type="text" formControlName="rua">

  <label>Cidade: </label>
  <input type="text" formControlName="cidade">

  <label>CEP: </label>
  <input type="text" formControlName="cep">
</fieldset>
...

PLunker: https://plnkr.co/edit/HfsjkE?p=preview

Adicionando Validadores

Um FormControl toma como parâmetros o valor padrão do campo, um validador síncrono ou array de validadores síncronos e um validador assíncrono ou um array de validadores assíncronos.

  registerForm = new FormGroup({
    primeironome: new FormControl('', [Validators.required, Validators.minLength(3)]),
    ...
  });

Não podemos esquecer de importar a classe Validators no nosso componente.

import { FormGroup, FormBuilder, Validators } from '@angular/forms';

Vamos colocar uma condição no nosso botão de enviar para que o formulário só possa ser submetido caso válido.

<button type="submit" [disabled]="registerForm.invalid">Enviar</button>

Os erros de preenchimento dos campos do formulário no template não vão aparecer por padrão se o programador não definir nada.

<p>      
<label>Primeiro Nome: </label>
<input type="text" formControlName="primeironome">
</p>
<div
  class="error"
  *ngIf="registerForm.get('primeironome').hasError('required') && registerForm.get('primeironome').touched">
  O campo nome é obrigatório.
</div>
<div
  class="error"
  *ngIf="registerForm.get('primeironome').hasError('minlength') && registerForm.get('primeironome').touched">
  Mínimo de 3 caracteres.
</div>

Nesse exemplo a diretiva ngIf testa o campo a cada mudança de foco e exibe a mensagem de erro caso necessário.

Plunker: https://plnkr.co/edit/1eFyis?p=preview

Existe uma maneira mais prática de criar os controladores do formulário no componente, que é através do uso de FormBuilder. Vamos incluir essa classe a partir de @angular/forms.

import { FormGroup, FormBuilder } from '@angular/forms';

Vamos injetar o FormBuilder no nosso construtor:

constructor(private formBuilder: FormBuilder) {}

E então podemos construir o modelo do formulário de forma mais simples como prometido:

this.registerForm = this.formBuilder.group({
  primeironome: ['', [Validators.required, Validators.minLength(3)]],
  sobrenome: '',
  endereco: this.formBuilder.group({
    rua: '',
    cidade: '',
    cep: ''
  })
});

Plunker: https://plnkr.co/edit/1eFyis?p=preview

Validadores Customizados

Um validador é uma função que toma como parâmetro um controle e retorna null se o controle for válido ou um objeto de erro se o controle não for válido. A interface Typescript para tal validador é algo do tipo:

interface Validator<T extends FormControl> {
   (c:T): {[error: string]:any};
}

Vamos implementar uma classe com uma função estática validadora validateEmail que implementa essa interface. Essa função vai receber um FormControl, confere se o seu valor casa com a expressão regular de endereço de email, retornando null em caso de sucesso e o obejto de erro caso contrário.

export class CustomEmailValidator {
  static validateEmail(control: Control): [[key: string]: boolean] {
    let pattern:RegExp = /\S+@\S+\.\S+/;
    return pattern.test(control.value) ? null : {"validateEmail": true};
  }
}

Depois é só adicionar esta função aos validadores do FormControl da mesma forma que um validador comum:

...
this.registerForm = this.formBuilder.group({
  email: ['', [Validators.required, CustomEmailValidator.validateEmail]]
});
...
}

Só não esqueça de importar a classe se você a tiver criado em outro arquivo.

Plunker: https://plnkr.co/edit/8mDdd9?p=preview

Diretivas de Validadores Customizados

Mas e se quisermos usar o validador customizado em um formulário orientado a template? Para isso vamos criar uma diretiva. Nosso validador deve poder ser usado assim:

<form novalidate>
  <input type="email" name="email" ngModel validateEmail>
</form>

validateEmail precisa ser inserido como um atributo do elemento DOM input. Para criar nossa diretiva, devemos importar o decorator @Directive de @angular/core e usá-lo em uma noma classe EmailValidator.

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

@Directive({
  selector: '[validateEmail][ngModel]'
})
export class EmailValidator {}

A nova diretiva deve ser declarada no nosso módulo:

...
import {EmailValidator} from './email.validator'
...
@NgModule({
...
  declarations: [ AppComponent, FormComponent, EmailValidator ],
...
})
export class AppModule {}

para poder ser importada no nosso componente. Se a diretiva for adicionada a um módulo comum/compartilhado que é importado pelo módulo que vai utilizar essa diretiva, devemos também exportá-la:

...
@NgModule({
...
  declarations: [ ..., EmailValidator ],
  exports: [ EmailValidator ]
...
})
export class SharedModule {}

Dando continuidade a nossa diretiva de validação de email, vamos criar uma classe que implementa a função validate. Esta função recebe um FormControl e deve retornar um objeto contendo o erro em caso da validação falhar ou nulo caso contrário.

export class EmailValidator {

  EMAIL_REGEXP = ...

  validate(c: FormControl) {
    return this.EMAIL_REGEXP.test(c.value) ? null : {
      validateEmail: {
        valid: false
      }
    };
  }
}

Note que desta vez a função não foi declarada estática. A condição para adicionar a classe à lista de providers da diretiva é que ela implemente a assinatura da função validate, tal como mencionado. Feito isso, a diretiva fica assim:

@Directive({
  selector: '[validateEmail][ngModel],[validateEmail][formControl]',
  providers: [
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => EmailValidator), multi: true }
  ]
})

No selector indicamos que a nossa diretiva validateEmail pode ser utilizada com as diretivas ngModel e formControl. Agora sobre os providers... O Angular tem um mecanismo interno para executar validadores em um controle de formulário. Ele mantém um multi provider para um token chamado NG_VALIDATORS.

Em resumo, um provider é uma instrução que descreve como um objeto é criado para determinado token. O parâmetro multi setado como true diz ao Angular que o token aceita múltiplos providers.

Geralmente, quanto temos múltiplos providers para um token, o último registrado é o que será utilizado. O Angular faz dessa forma para que possamos estender o que será injetado para determinado token.

E por último, o plunker para teste:

Plunker: https://plnkr.co/edit/BFdDzC?p=preview

results matching ""

    No results matching ""