Atualmente são apenas duas páginas, mas ao longo dos tutoriais outras serão desenvolvidas.
Inicializando a aplicação
$ ng-serve
Após carregar acesse http://localhost:4200/ leia atentamente o conteúdo da página Reactive Forms, em seguida navegue para a página Formulário Simples, leia e realize testes no formulário.
Os passos acima informados são extremamente necessários para o completo entendimento deste tutorial.
Arquitetura
Leia os comentários de cada método dos arquivos presentes neste projeto, isso irá ajudar muito na compreensão do que foi feito.
Levando em consideração que o ".html" seja o ponto de partida para visualizarmos uma página, quero propor a seguinte abordagem aqui, que será mostrar do arquivo mais interno ao mais externo. Ou seja, vamos começar pelo serviço, passando para o componente até chegar na página ".html".
abstract-form-builder.ts
É esta classe que irá fazer toda a "magia" na criação do objeto FormGroup. Em resumo sua função é criar o FormGroup do tipo <T> do qual será passado pela classe que for sua herdeira. Seu método principal é o construirFormGroup() além de criar o FormGroup ele cria uma referência local através do método atualizarReferenciaFormulario() capturando os FormControls e passando para a variável local formControls do tipo <T>. Além disso são inicializadas as validações padrão dos FormControls criados, quando houverem.
abstract-form-builder.ts
import {Injectable} from "@angular/core";
import {AbstractControl, FormBuilder, FormGroup} from "@angular/forms";
@Injectable()
export abstract class AbstractFormBuilder<T> {
formControls: T;
constructor(
private formBuilder: FormBuilder
) { }
/**
* Quem implementar será obrigado a informar se tem alguma validação
* padrão a ser feita no form
*/
abstract inicializarValidacoesDefault();
/**
* Aqui Passamos um objeto com os atributos que queremos para o
* nosso formulário, neste método será criado um FromGroup
*
* @param objetoReferencia Objeto que dará inicio a magia
* @return: FormGroup
*/
construirFormGroup(objetoReferencia: T): FormGroup{
const form = this.formBuilder.group(objetoReferencia);
this.atualizarReferenciaFormulario(form, objetoReferencia);
this.inicializarValidacoesDefault();
return form;
}
/**
* Captura os controls criados e passam para um objeto como tipo T,
* desta forma evita-se o comum uso de "this.formGroup.get('nomeAtributo')"
* e passa-se a usar um obejeto com os atributos necessários
*
* @param form
* @param objetoReferencia
*/
atualizarReferenciaFormulario(form: FormGroup, objetoReferencia: T){
Object.entries(objetoReferencia).forEach(field => {
const label = field[0];
objetoReferencia[label] = form.controls[label];
});
this.formControls = objetoReferencia;
}
/**
* Ao receber um Control seus validators serão substituidos pelos informados
*
* @param control
* @param novoValidador
*/
atualizarValidadores(control: AbstractControl, novoValidador: any | any[]){
control.setValidators(novoValidador);
control.updateValueAndValidity();
}
}
formulario-simples-builder-service.ts
formulario-simples-builder-service.ts
import {Injectable} from "@angular/core";
import {AbstractFormBuilder} from "src/app/shared/form-builer/abstract-form-builer";
import {Validators} from '@angular/forms';
/**
* Model, DTO, VM... Tanto faz!
* Independente da sigla que for usar a idea é ter um objeto que pode
* ser usado como coringa.
* Vamos entender melhor no artigo
*/
export class PessoaVM {
constructor(
public nome?: any,
public apelido?: any,
public altura?: any
) { }
}
/**
* A ideia é armazenar nesse serviço todas as regras de validações do formulário
*/
@Injectable()
export class FormularioSimplesBuilderService extends AbstractFormBuilder<PessoaVM> {
/** Podem ser um ou vários validator como para o campo default */
private alturaValidatorDefault = Validators.required;
/**
* Regra de negócio 1: os campos nome e altura são de preenchimento obrigatório
*/
inicializarValidacoesDefault() {
this.atualizarValidadores(this.formControls.nome, Validators.required);
this.atualizarValidadores(this.formControls.altura, this.alturaValidatorDefault);
}
/**
* Regra de negócio 2: Quando o apelido for preenchido então a altura passa
* a ter uma validação mínima de 1.20 e máxima de 2.20.
* Caso contrário então passa o validator default
*
* obs: repare no this.formControls.apelido.value "apeli passou a ter um novo
* atributo o '.value' isso porque no objeto 'formControls' ele tem uma
* referência de um FormControl".
*/
atualizarComportamentoControlAltura() {
const alturaValidators = this.formControls.apelido?.value ? [
this.alturaValidatorDefault,
Validators.min(1.20),
Validators.max(2.20)
] : this.alturaValidatorDefault;
this.atualizarValidadores(this.formControls.altura, alturaValidators);
}
}
O Serviço FormularioSimplesBuilderService é filho de AbstractFormBuilder, ou seja, seus comportamentos serão herdados e é nele que devemos aplicar todas as regras de validação do formulário. Isso o torna um serviço especializado em regras de negócio especificas do formulário que estamos desenvolvendo o que irá tornar o código mais legível e com baixo acoplamento.
campo-minado-component.ts
/**
* Imagina só sua classe component cheia desses métodos get('altura') pra cá
* get('altura') pra lá e de repente o atributo muda de "altura" para
* "alturaDaPessoa" você vai precisar de uma boa IDE para te ajudar arrumar isso.
*/
modificarComportamentoFormulario() {
this.pessoaFormGroup.get('altura').setValidators(
this.pessoaFormGroup.get('apelido').value ? [
Validators.required,
Validators.min(1.20),
Validators.max(2.20)
] : Validators.required
);
}
Vamos usar um objeto como referência que é melhor né!? Usamos o this.formControls objeto que foi herdado de AbstractFormBuilder dai colocamos this.formControls.altura se mudar já vai aparecer como erro de compilação, entretanto, será mais difícil isso acontecer pois a maioria das IDE's são eficientes no seu Refactor > Renamee conseguem atualizar todas as referências.
formulario-simples.component.ts
Nesta classe iremos injetar o FormularioSiplesBuilderService, usar o método construirFormGroup() e armazenar numa variável local do tipo FormGroup. Além disso a classe não tem mais preocupação nenhuma em como serão feitas as validações do formulário, mas apenas em quando serão feitas.
formulario-simples.component.ts
import {Component, OnInit} from '@angular/core';
import {FormGroup} from "@angular/forms";
import {FormularioSimplesBuilderService, PessoaVM} from "./formulario-simples-builder-service";
@Component({
selector: 'app-formulario-simples',
templateUrl: './formulario-simples.component.html',
styleUrls: ['./formulario-simples.component.css'],
providers: [FormularioSimplesBuilderService]
})
export class FormularioSimplesComponent implements OnInit {
pessoaFormGroup: FormGroup;
exibeTextoSubmissao: boolean = false;
constructor(
private formularioSimplesBuilderService: FormularioSimplesBuilderService
) {
}
ngOnInit(): void {
this.inicializarFormulario();
}
/**
* Dica: sempre bom separar bem os métodos e suas responsabilidades, alguns
* programadores costumam criar métodos gigantes que fazem milhares de
* coisas ao mesmo tempo. Não seja essa pessoa!
*/
private inicializarFormulario() {
this.pessoaFormGroup = this.formularioSimplesBuilderService.construirFormGroup(new PessoaVM());
}
/**
* Retornar os valores do formulário no formato Json
*/
verificarValoresFormulario() {
return JSON.stringify(this.pessoaFormGroup?.value, null, 2);
}
/**
* Este método esta sendo chamado pelo html toda vez que é digitado alguma
* iformação no campo apelido.As regras de como os campos devem se comportar
* estão no FormulárioSimplesBuilderService
*/
modificarComportamentoFormulario() {
this.formularioSimplesBuilderService.atualizarComportamentoControlAltura();
}
/**
* Nesse método poderiamos chamar métodos que irão tratar as informações
* adquiridas no formulário e em seguinda utilizar um serviço para enviar
* para o back-end (Quem sabe a gente não faz isso um dia)
*/
submeterFormulario() {
this.exibeTextoSubmissao = true;
}
}
formulario-simples.component.html
Finalmente o "html" aqui não temos nada novo, o formulário foi apresentado assim como qualquer exemplo que podemos encontrar no site do Angular.
formulario-simples.component.html
<!-- FORMULÁRIO - estão sendo utilizadas classes de estilização do Bootstrap
e campos do Angular Material -->
<div class="container-fluid d-flex justify-content-center">
<div class="card bg-light">
<div class="card-header text-center">
<h2 class="card-title">Cadastrar Pessoa</h2>
</div>
<div class="card-body">
<form class="form-group" [formGroup]="pessoaFormGroup"
(ngSubmit)="submeterFormulario()">
<div class="row">
<mat-form-field class="col">
<mat-label>Nome</mat-label>
<input matInput type="text" formControlName="nome">
</mat-form-field>
</div>
<div class="row">
<mat-form-field class="col-9">
<mat-label>Apelido</mat-label>
<input (keyup)="modificarComportamentoFormulario()"
formControlName="apelido" matInput type="text">
</mat-form-field>
<mat-form-field class="col-3">
<mat-label>Altura</mat-label>
<input matInput type="number" formControlName="altura">
</mat-form-field>
</div>
<div class="text-right mt-2">
<button mat-raised-button class="btn btn-success" type="submit"
[disabled]="pessoaFormGroup.invalid">Salvar
</button>
</div>
</form>
</div>
<div class="alert alert-info" role="alert">
<div class="row d-flex justify-content-center">
<strong>Valores (JSON)</strong>
</div>
<div class="row d-flex justify-content-center">
<pre>{{verificarValoresFormulario()}}</pre>
</div>
</div>
</div>
</div>
<div class="container-fluid mt-2">
<div *ngIf="exibeTextoSubmissao" class="alert alert-danger">
<button mat-mini-fab color="warn" class="float-right"
(click)="exibeTextoSubmissao = !exibeTextoSubmissao">
<mat-icon>close</mat-icon>
</button>
<h4 class="alert-heading">
<span class="material-icons">device_unknown</span>️
Vish!! Acho que tá faltando alguma coisa
</h4>
<hr>
<p>
Vi que você tentou salvar, só que eu não fiz essa parte. <span class="material-icons">pest_control</span>
<br>
Caso queira avançar sugiro que crie um serviço para enviar as informações do front-end
para o back-end através de uma requisição http.
</p>
<p>Ps: se valer de alguma ajuda eu já fiz um método chamado "submeterFormulario" e acho você pode usá-lo para
salvar.
<br>
Boa sorte! <span class="material-icons">directions_run</span></p>
</div>
<div class="alert alert-warning" role="alert">
<h4>Regras de negócio aplicadas:</h4>
<ul>
<li>O Campos nome e altura são de preenchimento obrigatório</li>
<li>Ao preencher o apelido o campo altura passa a ter um valor mínimo de 1.20 e máximo de 2.20</li>
</ul>
<hr>
<h4><span class="material-icons">contact_support</span> Dúvidas</h4>
<h5 class="alert-heading"> - Quem vai pedir um cadastro com uma regra tão estranha assim?</h5>
<p>
De fato a regra da altura é estranha, mas acredite quando eu digo que seu cliente poderá
ter requisitos bem peculiares. <span class="material-icons">sentiment_satisfied</span>
</p>
<h5 class="alert-heading"> - Onde foram parar os valores digitados?</h5>
<p>
Estão no objeto do tipo FormGroup e também estão no objeto formControls da classe FormularioSimplesBuilderService
que criamos para facilitar nossa vida. Vamos entender melhor sobre isso durante o tutorial.
</p>
</div>
</div>
Reparem que criei uma classe chamada PessoaVM (é ela que será o tipo <T>) no mesmo arquivo, mas se preferir pode colocar dentro de um arquivo próprio. Sua função é ser um modelo para nosso formulário, lembre-se que um dos problemas apontados no artigo anterior foi justamente o fato dos atributos serem criados em tempo de execução. Agora não mais!
A beleza de guardarmos uma referência dos FormControls dentro do objeto formControls é que não precisaremos usar o método get() do FormGroup passando uma string e correndo o risco de ter que replicar esse código diversas vezes. Dependendo da quantidade de regras de validação de um determinado campo esse get() irá sair caro pra você e é nesse ponto que está o campo minado.
O método modificarComportamentoFormulário() será executado quando o campo apelido for modificado então ele irá chamar o método atualizarComportamentoControlAltura(), pois esse sim sabe como manipular as validações. Olha que maravilha, o componente não precisa se preocupar em como fazer ele só delega e nosso Serviço especializado se encarrega de mostrar para o que veio.
Reitero a importância de você abrir o projeto, executar e ler seu conteúdo, para adquirir um completo entendimento acerca deste artigo. Se não o fez, volte e faça agora, eu espero.
Caso tenha gostado da solução e deseje implementá-la no seu projeto sugiro você verificar se os objetos recebidos nos métodos são nulos. É importante, mas no exemplo eu não fiz.
Obrigado por chegar até aqui! Aguardem para mais artigos.