DEV Community

Cover image for Introdução aos Reactive Forms!
Felipe Carvalho
Felipe Carvalho

Posted on

Introdução aos Reactive Forms!

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"]
}
Enter fullscreen mode Exit fullscreen mode

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 { }
Enter fullscreen mode Exit fullscreen mode

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([])
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;    
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Já a propriedade telefones simplesmente retorna a lista de controles adicionados ao FormArray.

get telefones() {
  return (<FormArray>this.usuarioForm.get("telefones")).controls;
}
Enter fullscreen mode Exit fullscreen mode

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": []
  });
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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;
  });
Enter fullscreen mode Exit fullscreen mode

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)