Trabalhando com formulários Angular 4: aninhamento e validação de entrada
Publicados: 2022-03-11Na web, alguns dos primeiros elementos de entrada do usuário eram um botão, caixa de seleção, entrada de texto e botões de opção. Até hoje, esses elementos ainda são usados em aplicativos da Web modernos, embora o padrão HTML tenha percorrido um longo caminho desde sua definição inicial e agora permita todos os tipos de interações sofisticadas.
Validar as entradas do usuário é uma parte essencial de qualquer aplicativo web robusto.
Formulários em aplicativos Angular podem agregar o estado de todas as entradas que estão sob esse formulário e fornecer um estado geral, como o status de validação do formulário completo. Isso pode ser muito útil para decidir se a entrada do usuário será aceita ou rejeitada sem verificar cada entrada separadamente.
Neste artigo, você aprenderá como trabalhar com formulários e realizar a validação de formulários com facilidade em seu aplicativo Angular.
No Angular 4, existem dois tipos diferentes de formulários disponíveis para trabalhar: formulários orientados por modelo e formulários reativos. Passaremos por cada tipo de formulário usando o mesmo exemplo para ver como as mesmas coisas podem ser implementadas de maneiras diferentes. Mais tarde, neste artigo, veremos uma nova abordagem sobre como configurar e trabalhar com formulários aninhados.
4 formas angulares
Em Angular 4, os quatro status a seguir são comumente usados por formulários:
válido – estado da validade de todos os controles de formulário, verdadeiro se todos os controles forem válidos
inválido – inverso de
valid
; true se algum controle for inválidopristine – dá um status sobre a “limpeza” da forma; true se nenhum controle foi modificado
sujo – inverso de
pristine
; true se algum controle foi modificado
Vamos dar uma olhada em um exemplo básico de um formulário:
<form> <div> <label>Name</label> <input type="text" name="name"/> </div> <div> <label>Birth Year</label> <input type="text" name="birthYear"/> </div> <div> <h3>Location</h3> <div> <label>Country</label> <input type="text" name="country"/> </div> <div> <label>City</label> <input type="text" name="city"/> </div> </div> <div> <h3>Phone numbers</h3> <div> <label>Phone number 1</label> <input type="text" name="phoneNumber[1]"/> <button type="button">remove</button> </div> <button type="button">Add phone number</button> </div> <button type="submit">Register</button> <button type="button">Print to console</button> </form>
A especificação para este exemplo é a seguinte:
nome - é obrigatório e único entre todos os usuários registrados
birthYear - deve ser um número válido e o usuário deve ter pelo menos 18 e menos de 85 anos
country - é obrigatório, e só para complicar um pouco as coisas, precisamos de uma validação de que se o país é a França, então a cidade deve ser Paris (digamos que nosso serviço é oferecido apenas em Paris)
phoneNumber – cada número de telefone deve seguir um padrão especificado, deve haver pelo menos um número de telefone e o usuário pode adicionar um novo ou remover um número de telefone existente.
O botão “Cadastre-se” é habilitado somente se todas as entradas forem válidas e, uma vez clicado, envia o formulário.
O “Print to Console” apenas imprime o valor de todas as entradas para o console quando clicado.
O objetivo final é implementar totalmente a especificação definida.
Formulários baseados em modelos
Os formulários orientados a modelos são muito semelhantes aos formulários do AngularJS (ou Angular 1, como alguns se referem a ele). Portanto, alguém que tenha trabalhado com formulários no AngularJS estará muito familiarizado com essa abordagem de trabalhar com formulários.
Com a introdução de módulos no Angular 4, é imposto que cada tipo específico de formulário esteja em um módulo separado e devemos definir explicitamente qual tipo vamos usar importando o módulo apropriado. Esse módulo para os formulários orientados por modelo é FormsModule. Dito isto, você pode ativar os formulários orientados por modelo da seguinte forma:
import {FormsModule} from '@angular/forms' import {NgModule} from '@angular/core' import {BrowserModule} from '@angular/platform-browser' import {AppComponent} from 'src/app.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent], bootstrap: [ AppComponent ] }) export class AppModule {}
Conforme apresentado neste trecho de código, primeiro devemos importar o módulo do navegador, pois ele “fornece serviços essenciais para iniciar e executar um aplicativo de navegador”. (dos documentos do Angular 4). Em seguida, importamos o FormsModule necessário para ativar os formulários orientados por modelo. E por último está a declaração do componente raiz, AppComponent, onde nos próximos passos iremos implementar o formulário.
Tenha em mente que neste exemplo e nos exemplos a seguir, você deve certificar-se de que o aplicativo seja inicializado corretamente usando o método platformBrowserDynamic
.
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; import {AppModule} from './app.module'; platformBrowserDynamic().bootstrapModule(AppModule);
Podemos supor que nosso AppComponent (app.component.ts) se parece com isso:
import {Component} from '@angular/core' @Component({ selector: 'my-app', templateUrl: 'src/app.component.tpl.html' }) export class AppComponent { }
Onde o template deste componente está no app.component.tpl.html e podemos copiar o template inicial para este arquivo.
Observe que cada elemento de entrada deve ter o atributo name
para ser identificado corretamente no formulário. Embora pareça um formulário HTML simples, já definimos um formulário compatível com Angular 4 (talvez você ainda não o veja). Quando o FormsModule é importado, o Angular 4 detecta automaticamente um elemento HTML de form
e anexa o componente NgForm a esse elemento (pelo selector
do componente NgForm). É o caso do nosso exemplo. Embora este formulário Angular 4 seja declarado, neste momento ele não conhece nenhuma entrada suportada pelo Angular 4. Angular 4 não é tão invasivo para registrar cada elemento HTML de input
para o ancestral de form
mais próximo.
A chave que permite que um elemento input seja percebido como um elemento Angular 4 e registrado no componente NgForm é a diretiva NgModel. Assim, podemos estender o template app.component.tpl.html da seguinte forma:
<form> .. <input type="text" name="name" ngModel> .. <input type="text" name="birthYear" ngModel > .. <input type="text" name="country" ngModel/> .. <input type="text" name="city" ngModel/> .. <input type="text" name="phoneNumber[1]" ngModel/> </form>
Ao adicionar a diretiva NgModel, todas as entradas são registradas no componente NgForm. Com isso, definimos um formulário Angular 4 totalmente funcional e até agora, tudo bem, mas ainda não temos como acessar o componente NgForm e as funcionalidades que ele oferece. As duas principais funcionalidades oferecidas pelo NgForm são:
Recuperando os valores de todos os controles de entrada registrados
Recuperando o estado geral de todos os controles
Para expor o NgForm, podemos adicionar o seguinte ao elemento <form>:
<form #myForm="ngForm"> .. </form>
Isso é possível graças à propriedade exportAs
do decorador Component
.
Feito isso, podemos acessar os valores de todos os controles de entrada e estender o modelo para:
<form #myForm="ngForm"> .. <pre>{{myForm.value | json}}</pre> </form>
Com myForm.value
estamos acessando dados JSON contendo os valores de todas as entradas registradas, e com {{myForm.value | json}}
{{myForm.value | json}}
, estamos imprimindo o JSON com os valores.
E se quisermos ter um subgrupo de entradas de um contexto específico envolvido em um contêiner e um objeto separado nos valores JSON, por exemplo, local contendo país e cidade ou os números de telefone? Não se estresse - os formulários orientados a modelos em Angular 4 também cobrem isso. A maneira de conseguir isso é usando a diretiva ngModelGroup
.
<form #myForm="ngForm"> .. <div ngModelGroup="location"> .. </div> </div ngModelGroup="phoneNumbers"> .. <div> .. </form>
O que nos falta agora é uma maneira de adicionar vários números de telefone. A melhor maneira de fazer isso seria usar uma matriz, como a melhor representação de um contêiner iterável de vários objetos, mas no momento da redação deste artigo, esse recurso não foi implementado para os formulários orientados por modelo. Então, temos que aplicar uma solução alternativa para fazer isso funcionar. A seção de números de telefone precisa ser atualizada da seguinte forma:
<div ngModelGroup="phoneNumbers"> <h3>Phone numbers</h3> <div *ngFor="let phoneId of phoneNumberIds; let i=index;"> <label>Phone number {{i + 1}}</label> <input type="text" name="phoneNumber[{{phoneId}}]" #phoneNumber="ngModel" ngModel/> <button type="button" (click)="remove(i); myForm.control.markAsTouched()">remove</button> </div> <button type="button" (click)="add(); myForm.control.markAsTouched()">Add phone number</button> </div>
O myForm.control.markAsTouched()
é usado para fazer com que o formulário seja touched
para que possamos exibir os erros naquele momento. Os botões não ativam esta propriedade quando clicados, apenas as entradas. Para tornar os próximos exemplos mais claros, não adicionarei esta linha no manipulador de cliques para add()
e remove()
. Basta imaginar que está lá. (Está presente nos Plunkers.)
Também precisamos atualizar o AppComponent
para conter o seguinte código:
private count:number = 1; phoneNumberIds:number[] = [1]; remove(i:number) { this.phoneNumberIds.splice(i, 1); } add() { this.phoneNumberIds.push(++this.count); }
Devemos armazenar um ID exclusivo para cada novo número de telefone adicionado e no *ngFor
, rastrear os controles do número de telefone por seu ID (admito que não seja muito legal, mas até que a equipe do Angular 4 implemente esse recurso, receio , é o melhor que podemos fazer)
Ok, o que temos até agora, adicionamos o formulário suportado pelo Angular 4 com entradas, adicionamos um agrupamento específico das entradas (localização e números de telefone) e expusemos o formulário dentro do modelo. Mas e se quisermos acessar o objeto NgForm em algum método do componente? Veremos duas maneiras de fazer isso.
Pela primeira forma, o NgForm, rotulado como myForm
no exemplo atual, pode ser passado como argumento para a função que servirá como manipulador para o evento onSubmit do formulário. Para uma melhor integração, o evento wrapped
é encapsulado por um evento específico do Angular 4, NgForm chamado ngSubmit
, e este é o caminho certo a seguir se quisermos executar alguma ação no envio. Então, o exemplo agora ficará assim:
<form #myForm="ngForm" (ngSubmit)="register(myForm)"> … </form>
Devemos ter um register
de método correspondente, implementado no AppComponent. Algo como:
register (myForm: NgForm) { console.log('Successful registration'); console.log(myForm); }
Dessa forma, aproveitando o evento onSubmit, temos acesso ao componente NgForm somente quando o submit é executado.
A segunda maneira é usar uma consulta de exibição adicionando o decorador @ViewChild a uma propriedade do componente.
@ViewChild('myForm') private myForm: NgForm;
Com essa abordagem, temos acesso ao formulário, independentemente de o evento onSubmit ter sido acionado ou não.
Excelente! Agora temos um formulário Angular 4 totalmente funcional com acesso ao formulário no componente. Mas, você percebe algo faltando? E se o usuário digitar algo como “isto-não-é-um-ano” na entrada “anos”? Sim, você entendeu, falta validação das entradas e abordaremos isso na seção a seguir.
Validação
A validação é muito importante para cada aplicação. Sempre queremos validar a entrada do usuário (não podemos confiar no usuário) para evitar enviar/salvar dados inválidos e devemos mostrar alguma mensagem significativa sobre o erro para orientar adequadamente o usuário a inserir dados válidos.
Para que alguma regra de validação seja aplicada em alguma entrada, o validador adequado deve estar associado a essa entrada. O Angular 4 já oferece um conjunto de validadores comuns como: required
, maxLength
, minLength
…
Então, como podemos associar um validador a uma entrada? Bem, muito fácil; basta adicionar a diretiva do validador ao controle:
<input name="name" ngModel required/>
Este exemplo torna a entrada “nome” obrigatória. Vamos adicionar algumas validações a todas as entradas em nosso exemplo.
<form #myForm="ngForm" (ngSubmit)="actionOnSubmit(myForm)" novalidate> <p>Is "myForm" valid? {{myForm.valid}}</p> .. <input type="text" name="name" ngModel required/> .. <input type="text" name="birthYear" ngModel required pattern="\\d{4,4}"/> .. <div ngModelGroup="location"> .. <input type="text" name="country" ngModel required/> .. <input type="text" name="city" ngModel/> </div> <div ngModelGroup="phoneNumbers"> .. <input type="text" name="phoneNumber[{{phoneId}}]" ngModel required/> .. </div> .. </form>
Nota:
novalidate
é usado para desabilitar a validação de formulário nativo do navegador.
Tornamos o “nome” obrigatório, o campo “anos” é obrigatório e deve conter apenas números, a entrada do país é obrigatória e também o número de telefone. Além disso, imprimimos o status da validade do formulário com {{myForm.valid}}
.
Uma melhoria neste exemplo seria também mostrar o que está errado com a entrada do usuário (não apenas mostrar o estado geral). Antes de continuarmos adicionando validação adicional, gostaria de implementar um componente auxiliar que nos permitirá imprimir todos os erros de um controle fornecido.
// show-errors.component.ts import { Component, Input } from '@angular/core'; import { AbstractControlDirective, AbstractControl } from '@angular/forms'; @Component({ selector: 'show-errors', template: ` <ul *ngIf="shouldShowErrors()"> <li *ngFor="let error of listOfErrors()">{{error}}</li> </ul> `, }) export class ShowErrorsComponent { private static readonly errorMessages = { 'required': () => 'This field is required', 'minlength': (params) => 'The min number of characters is ' + params.requiredLength, 'maxlength': (params) => 'The max allowed number of characters is ' + params.requiredLength, 'pattern': (params) => 'The required pattern is: ' + params.requiredPattern, 'years': (params) => params.message, 'countryCity': (params) => params.message, 'uniqueName': (params) => params.message, 'telephoneNumbers': (params) => params.message, 'telephoneNumber': (params) => params.message }; @Input() private control: AbstractControlDirective | AbstractControl; shouldShowErrors(): boolean { return this.control && this.control.errors && (this.control.dirty || this.control.touched); } listOfErrors(): string[] { return Object.keys(this.control.errors) .map(field => this.getMessage(field, this.control.errors[field])); } private getMessage(type: string, params: any) { return ShowErrorsComponent.errorMessages[type](params); } }
A lista com erros é mostrada apenas se houver alguns erros existentes e a entrada estiver tocada ou suja.
A mensagem para cada erro é pesquisada em um mapa de mensagens predefinidas errorMessages
(adicionei todas as mensagens antecipadamente).
Este componente pode ser usado da seguinte forma:
<div> <label>Birth Year</label> <input type="text" name="birthYear" #birthYear="ngModel" ngModel required pattern="\\d{4,4}"/> <show-errors [control]="birthYear"></show-errors> </div>
Precisamos expor o NgModel para cada entrada e passá-lo para o componente que renderiza todos os erros. Você pode notar que neste exemplo usamos um padrão para verificar se o dado é um número; e se o usuário digitar “0000”? Esta seria uma entrada inválida. Além disso, faltam os validadores para um nome único, a estranha restrição do país (se país='França', então a cidade deve ser 'Paris'), padrão para um número de telefone correto e a validação de que pelo menos um número de telefone existe. Este é o momento certo para dar uma olhada nos validadores personalizados.
Angular 4 oferece uma interface que cada validador customizado deve implementar, a interface Validator (que surpresa!). A interface do Validador basicamente se parece com isso:
export interface Validator { validate(c: AbstractControl): ValidationErrors | null; registerOnValidatorChange?(fn: () => void): void; }
Onde cada implementação concreta DEVE implementar o método 'validate'. Esse método de validate
é realmente interessante sobre o que pode ser recebido como entrada e o que deve ser retornado como saída. A entrada é um AbstractControl, o que significa que o argumento pode ser qualquer tipo que estenda AbstractControl (FormGroup, FormControl e FormArray). A saída do método validate
deve ser null
ou undefined
(sem saída) se a entrada do usuário for válida ou retornar um objeto ValidationErrors
se a entrada do usuário for inválida. Com esse conhecimento, agora vamos implementar um validador de birthYear
personalizado.
import { Directive } from '@angular/core'; import { NG_VALIDATORS, FormControl, Validator, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[birthYear]', providers: [{provide: NG_VALIDATORS, useExisting: BirthYearValidatorDirective, multi: true}] }) export class BirthYearValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const numValue = Number(c.value); const currentYear = new Date().getFullYear(); const minYear = currentYear - 85; const maxYear = currentYear - 18; const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear; const message = { 'years': { 'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear } }; return isValid ? null : message; } }
Há algumas coisas para explicar aqui. Primeiro você pode notar que implementamos a interface do Validador. O método de validate
verifica se o usuário tem entre 18 e 85 anos até o ano de nascimento informado. Se a entrada for válida, será retornado null
, ou então será retornado um objeto contendo a mensagem de validação. E a última e mais importante parte é declarar esta diretiva como um Validador. Isso é feito no parâmetro “providers” do decorador @Directive. Este validador é fornecido como um valor do multi-provedor NG_VALIDATORS. Além disso, não se esqueça de declarar esta diretiva no NgModule. E agora podemos usar este validador da seguinte forma:
<input type="text" name="birthYear" #year="ngModel" ngModel required birthYear/>
Sim, simples assim!
Para o número de telefone, podemos validar o formato do número de telefone assim:
import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[telephoneNumber]', providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumberFormatValidatorDirective, multi: true}] }) export class TelephoneNumberFormatValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value); const message = { 'telephoneNumber': { 'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)' } }; return isValidPhoneNumber ? null : message; } }
Agora vêm as duas validações, para o país e o número de telefones. Notou algo comum para os dois? Ambos requerem mais de um controle para realizar a validação adequada. Bem, você se lembra da interface do Validador e o que dissemos sobre isso? O argumento do método validate
é AbstractControl, que pode ser uma entrada do usuário ou o próprio formulário. Isso cria a oportunidade de implementar um validador que usa vários controles para determinar o status de validação concreto.
import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[countryCity]', providers: [{provide: NG_VALIDATORS, useExisting: CountryCityValidatorDirective, multi: true}] }) export class CountryCityValidatorDirective implements Validator { validate(form: FormGroup): ValidationErrors { const countryControl = form.get('location.country'); const cityControl = form.get('location.city'); if (countryControl != null && cityControl != null) { const country = countryControl.value; const city = cityControl.value; let error = null; if (country === 'France' && city !== 'Paris') { error = 'If the country is France, the city must be Paris'; } const message = { 'countryCity': { 'message': error } }; return error ? message : null; } } }
Implementamos um novo validador, validador país-cidade. Você pode notar que agora como argumento o método validate recebe um FormGroup e desse FormGroup podemos recuperar as entradas necessárias para validação. O resto das coisas são muito semelhantes ao validador de entrada única.
O validador para o número de números de telefone ficará assim:
import { Directive } from '@angular/core'; import { NG_VALIDATORS, Validator, FormGroup, ValidationErrors, FormControl } from '@angular/forms'; @Directive({ selector: '[telephoneNumbers]', providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumbersValidatorDirective, multi: true}] }) export class TelephoneNumbersValidatorDirective implements Validator { validate(form: FormGroup): ValidationErrors { const message = { 'telephoneNumbers': { 'message': 'At least one telephone number must be entered' } }; const phoneNumbers = <FormGroup> form.get('phoneNumbers'); const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0; return hasPhoneNumbers ? null : message; } }
Podemos usá-los assim:
<form #myForm="ngForm" countryCity telephoneNumbers> .. </form>
Igual aos validadores de entrada, certo? Apenas agora aplicado ao formulário.
Você se lembra do componente ShowErrors? Nós o implementamos para funcionar com um AbstractControlDirective, o que significa que poderíamos reutilizá-lo para mostrar todos os erros associados diretamente a este formulário também. Tenha em mente que, neste momento, as únicas regras de validação diretamente associadas ao formulário são os números Country-city
e Telephone numbers
(os outros validadores estão associados aos controles específicos do formulário). Para imprimir todos os erros de formulário, faça o seguinte:
<form #myForm="ngForm" countryCity telephoneNumbers > <show-errors [control]="myForm"></show-errors> .. </form>
A última coisa que resta é a validação de um nome exclusivo. Isso é um pouco diferente; para verificar se o nome é exclusivo, provavelmente é necessária uma chamada ao back-end para verificar todos os nomes existentes. Isso classifica como uma operação assíncrona. Para isso, podemos reutilizar a técnica anterior para validadores customizados, bastando fazer com que a validate
retorne um objeto que será resolvido em algum momento futuro (promessa ou observável). No nosso caso, usaremos uma promessa:
import { Directive } from '@angular/core'; import { NG_ASYNC_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms'; @Directive({ selector: '[uniqueName]', providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: UniqueNameValidatorDirective, multi: true}] }) export class UniqueNameValidatorDirective implements Validator { validate(c: FormControl): ValidationErrors { const message = { 'uniqueName': { 'message': 'The name is not unique' } }; return new Promise(resolve => { setTimeout(() => { resolve(c.value === 'Existing' ? message : null); }, 1000); }); } }
Estamos esperando por 1 segundo e depois retornando um resultado. Semelhante aos validadores de sincronização, se a promessa for resolvida com null
, isso significa que a validação foi aprovada; se a promessa for resolvida com qualquer outra coisa, a validação falhou. Observe também que agora este validador está registrado em outro multi-provedor, o NG_ASYNC_VALIDATORS
. Uma propriedade útil dos formulários em relação aos validadores assíncronos é a propriedade pending
. Pode ser usado assim:
<button [disabled]="myForm.pending">Register</button>
Ele desabilitará o botão até que os validadores assíncronos sejam resolvidos.
Aqui está um Plunker contendo o AppComponent completo, o componente ShowErrors e todos os validadores.
Com esses exemplos, cobrimos a maioria dos casos de trabalho com formulários orientados por modelo. Mostramos que os formulários orientados a modelos são realmente semelhantes aos formulários do AngularJS (será muito fácil para os desenvolvedores do AngularJS migrarem). Com este tipo de formulário, é bastante fácil integrar formulários Angular 4 com programação mínima, principalmente com manipulações no template HTML.
Formulários Reativos
Os formulários reativos também eram conhecidos como formulários “orientados por modelo”, mas eu gosto de chamá-los de formulários “programáticos”, e logo você verá o porquê. Os formulários reativos são uma nova abordagem para o suporte de formulários Angular 4, portanto, diferentemente dos orientados a modelos, os desenvolvedores AngularJS não estarão familiarizados com esse tipo.
Podemos começar agora, lembra como os formulários orientados a templates tinham um módulo especial? Pois bem, os formulários reativos também possuem um módulo próprio, chamado ReactiveFormsModule e devem ser importados para ativar esse tipo de formulário.
import {ReactiveFormsModule} from '@angular/forms' import {NgModule} from '@angular/core' import {BrowserModule} from '@angular/platform-browser' import {AppComponent} from 'src/app.component'; @NgModule({ imports: [ BrowserModule, ReactiveFormsModule ], declarations: [ AppComponent], bootstrap: [ AppComponent ] }) export class AppModule {}
Além disso, não se esqueça de inicializar o aplicativo.
Podemos começar com o mesmo AppComponent e template da seção anterior.
Neste ponto, se o FormsModule não for importado (e certifique-se de que não seja), temos apenas um elemento de formulário HTML regular com alguns controles de formulário, sem mágica Angular aqui.
Chegamos ao ponto em que você perceberá por que gosto de chamar essa abordagem de “programática”. Para habilitar os formulários Angular 4, devemos declarar o objeto FormGroup manualmente e preenchê-lo com controles como este:

import { FormGroup, FormControl, FormArray, NgForm } from '@angular/forms'; import { Component, OnInit } from '@angular/core'; @Component({ selector: 'my-app', templateUrl: 'src/app.component.html' }) export class AppComponent implements OnInit { private myForm: FormGroup; constructor() { } ngOnInit() { this.myForm = new FormGroup({ 'name': new FormControl(), 'birthYear': new FormControl(), 'location': new FormGroup({ 'country': new FormControl(), 'city': new FormControl() }), 'phoneNumbers': new FormArray([new FormControl('')]) }); } printMyForm() { console.log(this.myForm); } register(myForm: NgForm) { console.log('Registration successful.'); console.log(myForm.value); } }
Os métodos printForm
e register
são os mesmos dos exemplos anteriores e serão usados nas próximas etapas. Os tipos de chave usados aqui são FormGroup, FormControl e FormArray. Esses três tipos são tudo o que precisamos para criar um FormGroup válido. O FormGroup é fácil; é um recipiente simples de controles. O FormControl também é fácil; é qualquer controle (por exemplo, entrada). E por último, o FormArray é a peça do quebra-cabeça que estava faltando na abordagem orientada por modelos. O FormArray permite manter um grupo de controles sem especificar uma chave concreta para cada controle, basicamente um array de controles (parece a coisa perfeita para os números de telefone, certo?).
Ao construir qualquer um desses três tipos, lembre-se desta regra dos 3. O construtor para cada tipo recebe três argumentos— value
, validador ou lista de validadores e validador assíncrono ou lista de validadores assíncronos, definidos no código:
constructor(value: any, validator?: ValidatorFn | ValidatorFn[], asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[]);
Para FormGroup, o value
é um objeto onde cada chave representa o nome de um controle e o valor é o próprio controle.
Para FormArray, o value
é uma matriz de controles.
Para FormControl, o value
é o valor inicial ou o estado inicial (objeto contendo um value
e uma propriedade disabled
) do controle.
Criamos o objeto FormGroup, mas o modelo ainda não está ciente desse objeto. A vinculação entre o FormGroup no componente e o template é feita com quatro diretivas: formGroup
, formControlName
, formGroupName
e formArrayName
, usadas assim:
<form [formGroup]="myForm" (ngSubmit)="register(myForm)"> <div> <label>Name</label> <input type="text" name="name" formControlName="name"> </div> <div> <label>Birth Year</label> <input type="text" name="birthYear" formControlName="birthYear"> </div> <div formGroupName="location"> <h3>Location</h3> <div> <label>Country</label> <input type="text" name="country" formControlName="country"> </div> <div> <label>City</label> <input type="text" name="city" formControlName="city"> </div> </div> <div formArrayName="phoneNumbers"> <h3>Phone numbers</h3> <div *ngFor="let phoneNumberControl of myForm.controls.phoneNumbers.controls; let i=index;"> <label>Phone number {{i + 1}}</label> <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i"> <button type="button" (click)="remove(i)">remove</button> </div> <button type="button" (click)="add()">Add phone number</button> </div> <pre>{{myForm.value | json}}</pre> <button type="submit">Register</button> <button type="button" (click)="printMyForm()">Print to console</button> </form>
Agora que temos o FormArray, você pode ver que podemos usar essa estrutura para renderizar todos os números de telefone.
E agora para adicionar o suporte para adicionar e remover números de telefone (no componente):
remove(i: number) { (<FormArray>this.myForm.get('phoneNumbers')).removeAt(i); } add() { (<FormArray>this.myForm.get('phoneNumbers')).push(new FormControl('')); }
Agora, temos uma forma reativa Angular 4 totalmente funcional. Observe a diferença dos formulários orientados a templates onde o FormGroup foi “criado no template” (pelo escaneamento da estrutura do template) e passado para o componente, nos formulários reativos é o contrário, o FormGroup completo é criado no componente, então “passado para o modelo” e vinculado aos controles correspondentes. Mas, novamente, temos o mesmo problema com a validação, um problema que será resolvido na próxima seção.
Validação
Quando se trata de validação, os formulários reativos são muito mais flexíveis do que os formulários orientados por modelo. Sem alterações adicionais, podemos reutilizar os mesmos validadores que foram implementados anteriormente (para o template-driven). Assim, adicionando as diretivas do validador, podemos ativar a mesma validação:
<form [formGroup]="myForm" (ngSubmit)="register(myForm)" countryCity telephoneNumbers novalidate> <input type="text" name="name" formControlName="name" required uniqueName> <show-errors [control]="myForm.controls.name"></show-errors> .. <input type="text" name="birthYear" formControlName="birthYear" required birthYear> <show-errors [control]="myForm.controls.birthYear"></show-errors> .. <div formGroupName="location"> .. <input type="text" name="country" formControlName="country" required> <show-errors [control]="myForm.controls.location.controls.country"></show-errors> .. <input type="text" name="city" formControlName="city"> .. </div> <div formArrayName="phoneNumbers"> <h3>Phone numbers</h3> .. <input type="text" name="phoneNumber[{{phoneId}}]" [formControlName]="i" required telephoneNumber> <show-errors [control]="phoneNumberControl"></show-errors> .. </div> .. </form>
Tenha em mente que agora não temos a diretiva NgModel para passar para o componente ShowErrors, mas o FormGroup completo já está construído e podemos passar o AbstractControl correto para recuperar os erros.
Aqui está um Plunker completo com esse tipo de validação para formulários reativos.
Mas não seria divertido se apenas reutilizássemos os validadores, certo? Veremos como especificar os validadores ao criar o grupo de formulários.
Lembre-se da regra “3s rule” que mencionamos sobre o construtor para FormGroup, FormControl e FormArray? Sim, dissemos que o construtor pode receber funções validadoras. Então, vamos tentar essa abordagem.
Primeiro, precisamos extrair as funções de validate
de todos os validadores em uma classe expondo-os como métodos estáticos:
import { FormArray, FormControl, FormGroup, ValidationErrors } from '@angular/forms'; export class CustomValidators { static birthYear(c: FormControl): ValidationErrors { const numValue = Number(c.value); const currentYear = new Date().getFullYear(); const minYear = currentYear - 85; const maxYear = currentYear - 18; const isValid = !isNaN(numValue) && numValue >= minYear && numValue <= maxYear; const message = { 'years': { 'message': 'The year must be a valid number between ' + minYear + ' and ' + maxYear } }; return isValid ? null : message; } static countryCity(form: FormGroup): ValidationErrors { const countryControl = form.get('location.country'); const cityControl = form.get('location.city'); if (countryControl != null && cityControl != null) { const country = countryControl.value; const city = cityControl.value; let error = null; if (country === 'France' && city !== 'Paris') { error = 'If the country is France, the city must be Paris'; } const message = { 'countryCity': { 'message': error } }; return error ? message : null; } } static uniqueName(c: FormControl): Promise<ValidationErrors> { const message = { 'uniqueName': { 'message': 'The name is not unique' } }; return new Promise(resolve => { setTimeout(() => { resolve(c.value === 'Existing' ? message : null); }, 1000); }); } static telephoneNumber(c: FormControl): ValidationErrors { const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value); const message = { 'telephoneNumber': { 'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)' } }; return isValidPhoneNumber ? null : message; } static telephoneNumbers(form: FormGroup): ValidationErrors { const message = { 'telephoneNumbers': { 'message': 'At least one telephone number must be entered' } }; const phoneNumbers = <FormArray>form.get('phoneNumbers'); const hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers.controls).length > 0; return hasPhoneNumbers ? null : message; } }
Now we can change the creation of 'myForm' to:
this.myForm = new FormGroup({ 'name': new FormControl('', Validators.required, CustomValidators.uniqueName), 'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]), 'location': new FormGroup({ 'country': new FormControl('', Validators.required), 'city': new FormControl() }), 'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()]) }, Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers]) );
Ver? The rule of “3s,” when defining a FormControl, multiple validators can be declared in an array, and if we want to add multiple validators to a FormGroup they must be “merged” using Validators.compose (also Validators.composeAsync is available). And, that's it, validation should be working completely. There's a Plunker for this example as well.
This goes out to everybody that hates the “new” word. For working with the reactive forms, there's a shortcut provided—a builder, to be more precise. The FormBuilder allows creating the complete FormGroup by using the “builder pattern.” And that can be done by changing the FormGroup construction like this:
constructor(private fb: FormBuilder) { } ngOnInit() { this.myForm = this.fb.group({ 'name': ['', Validators.required, CustomValidators.uniqueName], 'birthYear': ['', [Validators.required, CustomValidators.birthYear]], 'location': this.fb.group({ 'country': ['', Validators.required], 'city': '' }), 'phoneNumbers': this.fb.array([this.buildPhoneNumberComponent()]) }, { validator: Validators.compose([CustomValidators.countryCity, CustomValidators.telephoneNumbers]) } ); }
Not a very big improvement from the instantiation with “new,” but there it is. And, don't worry, there's a Plunker for this also.
In this second section, we had a look at reactive forms in Angular 4. As you may notice, it is a completely new approach towards adding support for forms. Even though it seems verbose, this approach gives the developer total control over the underlying structure that enables forms in Angular 4. Also, since the reactive forms are created manually in the component, they are exposed and provide an easy way to be tested and controlled, while this was not the case with the template-driven forms.
Nesting Forms
Nesting forms is in some cases useful and a required feature, mainly when the state (eg, validity) of a sub-group of controls needs to determined. Think about a tree of components; we might be interested in the validity of a certain component in the middle of that hierarchy. That would be really hard to achieve if we had a single form at the root component. But, oh boy, it is a sensitive manner on a couple of levels. First, nesting real HTML forms, according to the HTML specification, is not allowed. We might try to nest <form> elements. In some browsers it might actually work, but we cannot be sure that it will work on all browsers, since it is not in the HTML spec. In AngularJS, the way to work around this limitation was to use the ngForm
directive, which offered the AngularJS form functionalities (just grouping of the controls, not all form
capabilities like posting to the server) but could be placed on any element. Also, in AngularJS, nesting of forms (when I say forms, I mean NgForm) was available out of the box. Just by declaring a tree of couple of elements with the ngForm
directive, the state of each form was propagated upwards to the root element.
In the next section, we will have a look at a couple options on how to nest forms. I like to point out that we can differentiate two types of nesting: within the same component and across different components.
Nesting within the Same Component
If you take a look at the example that we implemented with the template-driven and the reactive approach, you will notice that we have two inner containers of controls, the “location” and the “phone numbers.” To create that container, to store the values in a separate property object, we used the NgModelGroup, FormGroupName, and the FormArrayName directives. If you have a good look at the definition of each directive, you may notice that each one of them extends the ControlContainer class (directly or indirectly). Well, what do you know, it turns out this is enough to provide the functionality that we require, wrapping up the state of all inner controls and propagating that state to the parent.
For the template-driven form, we need to do the following changes:
<form #myForm="ngForm" (ngSubmit)="register(myForm)" novalidate> .. <div ngModelGroup="location" #location="ngModelGroup" countryCity> .. <show-errors [control]="location"></show-errors> </div> <div ngModelGroup="phoneNumbers" #phoneNumbers="ngModelGroup" telephoneNumbers> .. <show-errors [control]="phoneNumbers"></show-errors> </div> </form>
We added the ShowErrors component to each group, to show the errors directly associated with that group only. Since we moved the countryCity
and telephoneNumbers
validators to a different level, we also need to update them appropriately:
// country-city-validator.directive.ts let countryControl = form.get('country'); let cityControl = form.get('city');
And telephone-numbers-validator.directive.ts to:
let phoneNumbers = form.controls; let hasPhoneNumbers = phoneNumbers && Object.keys(phoneNumbers).length > 0;
You can try the full example with template-driven forms in this Plunker.
And for the reactive forms, we will need some similar changes:
<form [formGroup]="myForm" (ngSubmit)="register(myForm)" novalidate> .. <div formGroupName="location"> .. <show-errors [control]="myForm.controls.location"></show-errors> </div> <div formArrayName="phoneNumbers"> .. <show-errors [control]="myForm.controls.phoneNumbers"></show-errors> </div> .. </form>
The same changes from country-city-validator.directive.ts
and telephone-numbers-validator.directive.ts
are required for the countryCity
and telephoneNumbers
validators in CustomValidators to properly locate the controls.
And lastly, we need to modify the construction of the FormGroup to:
this.myForm = new FormGroup({ 'name': new FormControl('', Validators.required, CustomValidators.uniqueName), 'birthYear': new FormControl('', [Validators.required, CustomValidators.birthYear]), 'location': new FormGroup({ 'country': new FormControl('', Validators.required), 'city': new FormControl() }, CustomValidators.countryCity), 'phoneNumbers': new FormArray([this.buildPhoneNumberComponent()], CustomValidators.telephoneNumbers) });
And there you have it—we've improved the validation for the reactive forms as well and as expected, the Plunker for this example.
Nesting across Different Components
It may come as a shock to all AngularJS developers, but in Angular 4, nesting of forms across different component doesn't work out of the box. I'm going to be straight honest with you; my opinion is that nesting is not supported for a reason (probably not because the Angular 4 team just forgot about it). Angular4's main enforced principle is a one-way data flow, top to bottom through the tree of components. The whole framework was designed like that, where the vital operation, the change detection, is executed in the same manner, top to bottom. If we follow this principle completely, we should have no issues, and all changes should be resolved within one full detection cycle. That's the idea, at least. In order to check that one-way data flow is implemented correctly, the nice guys in the Angular 4 team implemented a feature that after each change detection cycle, while in development mode, an additional round of change detection is triggered to check that no binding was changed as a result of reverse data propagation. What this means, let's think about a tree of components (C1, C2, C3, C4) as in Fig. 1, the change detection starts at the C1 component, continues at the C2 component and ends in the C3 component.
If we have some method in C3 with a side effect that changes some binding in C1, that means that we are pushing data upwards, but the change detection for C1 already passed. When working in dev mode, the second round kicks in and notices a change in C1 that came as a result of a method execution in some child component. Then you are in trouble and you'll probably see the “Expression has changed after it was checked” exception. You could just turn off the development mode and there will be no exception, but the problem will not be solved; plus, how would you sleep at night, just sweeping all your problems under the rug like that?
Depois de saber disso, pense no que estamos fazendo se agregarmos o estado dos formulários. Isso mesmo, os dados são empurrados para cima na árvore de componentes. Mesmo ao trabalhar com formulários de nível único, a integração dos controles de formulário ( ngModel
) e o próprio formulário não é tão boa. Eles acionam um ciclo adicional de detecção de alterações ao registrar ou atualizar o valor de um controle (isso é feito usando uma promessa resolvida, mas mantenha-a em segredo). Por que uma rodada adicional é necessária? Bem, pelo mesmo motivo, os dados estão fluindo para cima, do controle para o formulário. Mas, às vezes, aninhar formulários em vários componentes é um recurso necessário e precisamos pensar em uma solução para oferecer suporte a esse requisito.
Com o que sabemos até agora, a primeira ideia que vem à mente é usar formulários reativos, criar a árvore de formulários completa em algum componente raiz e depois passar os formulários filhos para os componentes filhos como entradas. Dessa forma, você acoplou fortemente o pai com os componentes filho e confundiu a lógica de negócios do componente raiz com a manipulação da criação de todos os formulários filho. Vamos lá, somos profissionais, tenho certeza que podemos descobrir uma maneira de criar componentes totalmente isolados com formulários e fornecer uma maneira de apenas propagar o estado para quem for o pai.
Tudo isso dito, aqui está uma diretiva que permite aninhar formulários Angular 4 (implementado porque era necessário para um projeto):
import { OnInit, OnDestroy, Directive, SkipSelf, Optional, Attribute, Injector, Input } from '@angular/core'; import { NgForm, FormArray, FormGroup, AbstractControl } from '@angular/forms'; const resolvedPromise = Promise.resolve(null); @Directive({ selector: '[nestableForm]' }) export class NestableFormDirective implements OnInit, OnDestroy { private static readonly FORM_ARRAY_NAME = 'CHILD_FORMS'; private currentForm: FormGroup; @Input() private formGroup: FormGroup; constructor(@SkipSelf() @Optional() private parentForm: NestableFormDirective, private injector: Injector, @Attribute('rootNestableForm') private isRoot) { } ngOnInit() { if (!this.currentForm) { // NOTE: at this point both NgForm and ReactiveFrom should be available this.executePostponed(() => this.resolveAndRegister()); } } ngOnDestroy() { this.executePostponed(() => this.parentForm.removeControl(this.currentForm)); } public registerNestedForm(control: AbstractControl): void { // NOTE: prevent circular reference (adding to itself) if (control === this.currentForm) { throw new Error('Trying to add itself! Nestable form can be added only on parent "NgForm" or "FormGroup".'); } (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME)).push(control); } public removeControl(control: AbstractControl): void { const array = (<FormArray>this.currentForm.get(NestableFormDirective.FORM_ARRAY_NAME)); const idx = array.controls.indexOf(control); array.removeAt(idx); } private resolveAndRegister(): void { this.currentForm = this.resolveCurrentForm(); this.currentForm.addControl(NestableFormDirective.FORM_ARRAY_NAME, new FormArray([])); this.registerToParent(); } private resolveCurrentForm(): FormGroup { // NOTE: template-driven or model-driven => determined by the formGroup input return this.formGroup ? this.formGroup : this.injector.get(NgForm).control; } private registerToParent(): void { if (this.parentForm != null && !this.isRoot) { this.parentForm.registerNestedForm(this.currentForm); } } private executePostponed(callback: () => void): void { resolvedPromise.then(() => callback()); } }
O exemplo no GIF a seguir mostra um componente main
contendo form-1
e, dentro desse formulário, há outro componente aninhado, component-2
. component-2
contém form-2
, que tem form-2.1
aninhado , form-2.2
, e um componente ( component-3
) que tem uma árvore de uma forma reativa nele e um componente ( component-4
) que contém um formulário que é isolado de todas as outras formas. Bastante confuso, eu sei, mas eu queria fazer um cenário bastante complexo para mostrar a funcionalidade desta diretiva.
O exemplo é implementado neste Plunker.
As funcionalidades que ele oferece são:
Habilita o aninhamento adicionando a diretiva nestableForm aos elementos: form, ngForm, [ngForm], [formGroup]
Funciona com formulários reativos e orientados por modelos
Permite a construção de uma árvore de formulários que abrange vários componentes
Isola uma subárvore de formulários com rootNestableForm=”true” (não será registrado no pai nestableForm)
Esta diretiva permite que um formulário em um componente filho se registre no primeiro pai nestebleForm, independentemente de o formulário pai ser declarado no mesmo componente ou não. Entraremos nos detalhes da implementação.
Primeiro, vamos dar uma olhada no construtor. O primeiro argumento é:
@SkipSelf() @Optional() private parentForm: NestableFormDirective
Isso procura o primeiro pai NestableFormDirective. @SkipSelf, para não corresponder a si mesmo, e @Optional porque pode não encontrar um pai, no caso do formulário raiz. Agora temos uma referência ao formulário aninhado pai.
O segundo argumento é:
private injector: Injector
O injetor é usado para recuperar o provider
FormGroup atual (modelo ou reativo).
E o último argumento é:
@Attribute('rootNestableForm') private isRoot
para obter o valor que determina se este formulário está isolado da árvore de formulários.
Em seguida, no ngInit
como uma ação adiada (lembra do fluxo de dados reverso?), o FormGroup atual é resolvido, um novo controle FormArray chamado CHILD_FORMS
é registrado neste FormGroup (onde os formulários filhos serão registrados) e como última ação, o O FormGroup atual é registrado como filho do formulário encaixável pai.
A ação ngOnDestroy
é executada quando o formulário é destruído. Ao destruir, novamente como uma ação adiada, o formulário atual é removido do pai (anulação do registro).
A diretiva para formulários encaixáveis pode ser customizada para uma necessidade específica—talvez remova o suporte para formulários reativos, registre cada formulário filho com um nome específico (não em um array CHILD_FORMS), e assim por diante. Esta implementação da diretiva nestableForm atendeu aos requisitos do projeto e é apresentada aqui como tal. Ele cobre alguns casos básicos como adicionar um novo formulário ou remover um formulário existente dinamicamente (*ngIf) e propagar o estado do formulário para o pai. Isso basicamente se resume a operações que podem ser resolvidas dentro de um ciclo de detecção de alterações (com adiamento ou não).
Se você quiser um cenário mais avançado, como adicionar uma validação condicional a alguma entrada (por exemplo, [required] = "someCondition") que exigiria 2 rodadas de detecção de alterações, não funcionará devido à regra "one-detection-cycle-resolution" imposta pelo Angular 4.
De qualquer forma, se você planeja usar esta diretiva, ou implementar alguma outra solução, tenha muito cuidado com as coisas que foram mencionadas relacionadas à detecção de alterações. Neste ponto, é assim que o Angular 4 é implementado. Pode mudar no futuro - não podemos saber. A configuração atual e a restrição imposta no Angular 4 mencionadas neste artigo podem ser uma desvantagem ou um benefício. Ele continua a ser visto.
Formulários Facilitados com Angular 4
Como você pode ver, a equipe do Angular fez um trabalho muito bom ao fornecer muitas funcionalidades relacionadas aos formulários. Espero que este post sirva como um guia completo para trabalhar com os diferentes tipos de formulários em Angular 4, também fornecendo informações sobre alguns conceitos mais avançados, como o aninhamento de formulários e o processo de detecção de alterações.
Apesar de todos os diferentes posts relacionados aos formulários do Angular 4 (ou qualquer outro assunto do Angular 4), na minha opinião, o melhor ponto de partida é a documentação oficial do Angular 4. Além disso, os caras do Angular têm uma boa documentação em seu código. Muitas vezes, encontrei uma solução apenas olhando para o código-fonte e a documentação lá, sem pesquisar no Google ou qualquer coisa. Sobre o aninhamento de formulários, discutido na última seção, acredito que qualquer desenvolvedor AngularJS que comece a aprender Angular 4 vai se deparar com esse problema em algum momento, que foi uma espécie de inspiração para escrever este post.
Como também vimos, existem dois tipos de formulários e não existe uma regra rígida de que você não possa usá-los juntos. É bom manter a base de código limpa e consistente, mas às vezes, algo pode ser feito mais facilmente com formulários orientados por modelo e, às vezes, é o contrário. Então, se você não se importa com os tamanhos de pacote um pouco maiores, sugiro usar o que você considerar mais apropriado caso a caso. Apenas não os misture dentro do mesmo componente, porque provavelmente levará a alguma confusão.
Plunkers usados neste post
Formulários baseados em modelos
Formulários reativos, validadores de modelos
Formulários reativos, validadores de código
Formulários reativos, construtor de formulários
Formulários orientados a modelos, aninhados no mesmo componente
Formulários reativos, aninhados no mesmo componente
Formulários aninhados por meio de árvore de componentes