Introdução aos Reactive Forms!
Tags: angular
Published: 2019-07-17 18:30:13

##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:
```json
{
    "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:
![](https://thepracticaldev.s3.amazonaws.com/i/oyrgvsofa257nu77af7d.PNG)

---

##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**:
```typescript
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 **FormControl**s, 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 **FormControl**s. *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.*

```typescript
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"**.

```html
<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:
```html
<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:

![](https://thepracticaldev.s3.amazonaws.com/i/edqq6wle8u7wlizx31y4.PNG)

É 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.
```css
input.ng-invalid.ng-touched {
    border-color: red;    
}
```

![](https://thepracticaldev.s3.amazonaws.com/i/41p44aj6b9ihys5d2s3i.PNG)

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.

```html
<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:
```typescript
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.

```html
<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()**.

```typescript
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**.

```typescript
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:

```typescript
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.

```typescript
  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:

```typescript
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.

```typescript
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](https://stackblitz.com/edit/angular-intro-reactive-forms?embed=1&file=src/app/app.component.ts).

{% stackblitz angular-intro-reactive-forms %}