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.
Top comments (0)