Introdução aos Reactive Forms!
Felipe Carvalho
Posted on July 17, 2019
Introdução
Com Reactive Forms, conseguimos alcançar um controle maior sobre formulários: ao contrário dos Template Driven Forms (também chamado de TD Forms) sua estrutura é criada no componente e adicionamos referências à essa estrutura no template, dessa forma, temos um controle maior e facilitado sobre seus elementos.
Além de controles básicos sobre o formulário (estado, limpar, preencher, etc.), os Reactive Forms nos fornece controle sobre:
- Campos (FormControls)
- Grupos de campos (FormGroups)
- Campos multi-valor com identificador próprio (FormArray)
- Validações customizadas síncronas
- Validações customizadas assíncronas (consultando servidor, por exemplo)
- Adição e remoção de controles a qualquer momento
- Manipulação de controles
Objetivo
Construir um formulário que atenda ao seguinte modelo:
{
"nome": "Felipe dos Santos Carvalho",
"endereco": "Rua Um",
"acesso": {
"email": "contato@felipecarvalho.net",
"senha": "senha"
},
"telefones": ["12345678", "23456789"]
}
Campos de telefones devem ser adicionados dinamicamente e deverão ser obrigatórios quando adicionados.
Todos campos devem ser obrigatórios, exceto endereço.
O campo email deve possuir validação de email.
O campo senha deve receber ao menos 3 caracteres.
Devem existir controles para preencher, limpar e enviar o formulário, além de visualizações do estado atual do formulário.
O formulário não ficará muito bonito, mas os controles ao redor dele facilitarão o entendimento:
Ele se parecerá com:
Implementação
Antes de tudo, o módulo ReactiveFormsModule deve ser incluído nos imports do módulo da aplicação. No caso, em app.module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
imports: [
BrowserModule,
ReactiveFormsModule
],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
Definindo a estrutura do formulário
No componente onde o formulário será exibido (app.component), uma variável foi criada para armazenar a estrutura do formulário. A estrutura foi instanciada no ngOnInit(). Basicamente foi criado um FormGroup com FormControls, um outro FormGroup e um FormArray.
FormControl: controle que será aplicado aos campos do formulário, podendo receber como parâmetro um valor inicial, array de validadores (inclusive customatizados) e um array de validadores assíncronos.
FormArray: controle para campos multi-valor, onde cada índice do array é um identificador para um valor. Agrupa FormControls. Obs.: importante ressaltar que não é o caso de um multi-select, para eles, o FormControl é suficiente.
FormGroup: agrupa os controles acima, além de outros FormGroup.
Obs.: FormArray e FormGroup também podem receber validadores, tal como o FormControl, mas, particularmente, nunca precisei usá-los e, para simplificar, não os abordarei.
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl, FormArray, Validators } from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
usuarioForm: FormGroup;
ngOnInit() {
this.usuarioForm = new FormGroup({
"nome": new FormControl("João", [Validators.required]),
"endereco": new FormControl(),
"acesso": new FormGroup({
"email": new FormControl(null, [Validators.required, Validators.email]),
"senha": new FormControl(null, [Validators.required, Validators.minLength(3)])
}),
"telefones": new FormArray([])
});
}
}
Adicionando referências ao template
Os controles definidos no componente agora devem ser aplicados aos campos no template.
- [formGroup]="usuarioForm" associa uma tag ao formulário criado.
- formControlName aponta para um dos controles criados: "nome", "endereco", "email" e "senha".
- formGroupName associa uma tag a um sub-grupo de dados, no caso, "acesso".
<form [formGroup]="usuarioForm" (ngSubmit)="enviar()">
<h5>Cadastro de usuário</h5>
<div class="form-row">
<div class="form-group col-12">
<label for="nome">Nome</label>
<input type="text" name="nome" id="nome" class="form-control" formControlName="nome">
</div>
</div>
<div class="form-row">
<div class="form-group col-12">
<label for="endereco">Endereço</label>
<input name="endereco" id="endereco" class="form-control" formControlName="endereco">
</div>
</div>
<div class="form-row" formGroupName="acesso">
<div class="form-group col-12 col-sm-6">
<label for="email">E-mail</label>
<input type="email" name="email" id="email" class="form-control" formControlName="email">
</div>
<div class="form-group col-12 col-sm-6">
<label for="senha">Senha</label>
<input type="password" name="senha" id="senha" class="form-control" formControlName="senha">
</div>
</div>
</form>
Botão submit
O formulário poderia estar associado a uma tag div comum, ao invés de uma tag form.
Também não seria necessário utilizar o ngSubmit() para chamar a função de envio do formulário, isso poderia ser feito em um botão comum ou a qualquer momento da aplicação, já que o o componente tem controle total sobre o estado atual do formulário.
Ao final do formulário, foi adicionado um botão que é desabilitado quando o formulário é não válido, através do !usuarioForm.valid. É bom ter em mente que também há a propriedade invalid, que, pode passar a impressão de que ela fornecerá o resultado esperado, porém... o formulário também possui o estado PENDING, que não é válido, mas não é inválido e ocorre quando alguma validação assíncrona está sendo processada/aguardada. Logo, se usada a propriedade invalid, o botão seria habilitado quando o formulário estivesse PENDING, permitindo o usuário enviar um formulário possivelmente incorreto!
Considerando isso, o botão ficou da seguinte forma:
<button type="submit" class="btn btn-primary mt-3" [disabled]="!usuarioForm.valid">Enviar</button>
Validações
Inspecionando os elementos, é possível observar que o Angular adiciona algumas classes aos inputs para identificar quem foi tocado e quem está inválido, por exemplo:
É comum usar isso para mudar o CSS dos campos inválidos.
Ao CSS do componente foi adicionado um estilo para que, quando os input estiverem inválidos e estiverem sido tocados, a cor da borda seja alterada para vermelho.
input.ng-invalid.ng-touched {
border-color: red;
}
Também foram adicionados alertas para senha inválida, e-mail inválido e para quando o grupo de acesso tiver algum controle inválido.
<div class="col-12" *ngIf="senhaInvalida">
<div class="alert alert-danger" role="alert">
<strong>Senha inválida!</strong>
</div>
</div>
<div class="col-12" *ngIf="emailInvalido">
<div class="alert alert-danger" role="alert">
<strong>Informe um e-mail válido!</strong>
</div>
</div>
<div class="col-12" *ngIf="acessoInvalidos">
<div class="alert alert-danger" role="alert">
<strong>Dados de acesso inválidos!</strong>
</div>
</div>
Os ngIf acessam as seguintes propriedades do componente:
get emailInvalido() {
return !this.usuarioForm.get('acesso.email').valid
&& this.usuarioForm.get('acesso.email').touched;
}
get senhaInvalida() {
return !this.usuarioForm.get('acesso.senha').valid
&& this.usuarioForm.get('acesso.senha').touched;
}
get acessoInvalidos() {
return !this.usuarioForm.get('acesso').valid
&& this.usuarioForm.get('acesso').touched;
}
FormArray de telefones
A implementação do FormArray consiste em demarcar onde ele começa através do uso da diretiva formArrayName apontando para o FormArray criado anteriormente, "telefone".
O array telefones será percorrido através do ngFor e, para cada elemento do FormArray telefones, um novo input será renderizado na tela, onde seu index será o seu identificador para o formControlName.
O método adicionarTelefone() será encarregado de adicionar novos elementos ao array.
<div class="row" formArrayName="telefones">
<div class="col-12">
<h4>
Telefones
<button type="button" class="btn btn-success ml-3" (click)="adicionarTelefone()">Adicionar</button>
</h4>
<div class="form-group mt-3" *ngFor="let telefone of telefones; let i = index">
<label>Telefone {{i + 1}}</label>
<input type="text" class="form-control" [formControlName]="i">
</div>
</div>
</div>
Em adicionarTelefone(), um novo controle é criado e adicionado ao array de telefones do usuarioForm. Observe que uma conversão para FormArray foi necessária: isso se deve ao fato de que o get() retorna um AbstractControl, que é herdado pelo FormArray, porém, não implementa o método push().
adicionarTelefone() {
const control = new FormControl(null, [Validators.required, Validators.minLength(8)]);
(<FormArray>this.usuarioForm.get("telefones")).push(control);
}
Já a propriedade telefones simplesmente retorna a lista de controles adicionados ao FormArray.
get telefones() {
return (<FormArray>this.usuarioForm.get("telefones")).controls;
}
Pré-definindo valores do formulário
Existem dois métodos para preencher o formulário e eles possuem uma sutil diferença:
- setValue(), ao qual devem ser fornecidos dados para TODOS os campos do formulário.
- patchValue(), ao qual você pode preencher parcialmente o formulário e, os dados que já nele estiverem, se não forem passados ao método, permanecerão intocados.
No topo do formulário, foram criados botões para testar o comportamento. Reparem que para o setValue(), se telefones tiverem sido adicionados, um erro é emitido no console.
Como os métodos recebem objetos de qualquer tipo, nada te impediria de passar um objeto instanciado anteriormente, desde que atenda aos requisitos.
A implementação ficou da seguinte forma:
preencherComPatchValue() {
this.usuarioForm.patchValue({
"nome": "Felipe dos Santos Carvalho",
"acesso": {
"email": "contato@felipecarvalho.net"
}
});
}
preencherComSetValue() {
this.usuarioForm.setValue({
"nome": "Felipe dos Santos Carvalho",
"endereco": "Rua Um",
"acesso": {
"email": "contato@felipecarvalho.net",
"senha": "senha"
},
"telefones": []
});
}
Limpando o formulário
Também foi criado um botão para limpar o formulário que simplesmente faz uma chamada ao método reset(). Dependendo da necessidade da sua aplicação, talvez você também possa precisar de alguns desses controles:
- markAsUntouched(), que retorna ao estado de que o usuário não passou por nenhum campo (não focou).
- markAsPristine(), que retorna ao estado de que o usuário não modificou nenhum campo - não forneceu nenhum valor.
limpar() {
this.usuarioForm.reset();
}
Tratando dados antes de enviar ao servidor
Como nos Reactive Forms, não associamos um modelo ao formulário, temos as seguintes opções: trabalhar com o dado bruto, obter o valor campo a campo ou fazer alguma conversão para um modelo idêntico ao criado.
No enviar() foi demonstrado algumas possibiliaddes de trabalhar com os dados:
enviar() {
console.log(this.usuarioForm.value);
this.usuarioModel = Object.assign(this.usuarioForm.value);
console.log(this.usuarioModel);
this.usuarioModel = <Usuario>this.usuarioForm.value;
console.log(this.usuarioModel);
const nome = this.usuarioForm.get("nome").value;
console.log(nome);
}
Detectando mudanças de valores e estado
As mudanças podem ser detectadas ao nos inscrevermos nos observables valueChanges, para valores e statusChanges para estado.
Foram criados contadores e variáveis para receberem os valores pelos observables retornados.
O valueChanges conta com um plus: foi adicionado um operador rxjs para que a função dentro do subscribe só seja executada 2 segundos após nenhuma nova alteração for detectada. Isso pode ser interessante para a implementação de um auto-save ou filtros a serem executados ao digitar.
this.usuarioForm.valueChanges
.pipe(debounceTime(2000))
.subscribe((valor) => {
this.countMudancaValor++;
this.ultimaMudancaValor = valor;
});
this.usuarioForm.statusChanges
.subscribe((status) => {
this.countMudancaStatus++;
this.ultimaMudancaStatus = status;
});
Vamos ver funcionando?
Aproveitei e adicionei alguns outros campos para servir de base para auumentar um pouco a complexidade.
Recomendo que abra em uma nova aba para ter uma melhor experiência. Faça isso clicando aqui.
Posted on July 17, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.