Formulário Simples

Vamos entender de uma vez por todas que forma "diferentona" é essa de que tanto estamos falando.

Pré-requisitos

  • Leia a página anterior Reactive Forms (Formulários Reativos)

  • Acesse angular-forms-example e faça download do projeto

  • Leia atentamente o README.md

Estrutura de páginas

  • Reactive Forms

  • Formulário Simples

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 > Rename e 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>

Last updated