Como construir um formulário dinâmico - Angular
Aprenda a criar um formulários dinâmico, como um questionário, efetuando as devidas validações através do FormBuilder.array.

Introdução
Trabalhar com formulários estáticos com certeza é algo bem comum na vida de um desenvolvedor. Dois bons exemplos são os formulários de login e cadastro. Ambos não mudam com tanta frequência, ou seja, dificilmente você irá acrescentar, editar ou remover campos. Desta forma deixá-lo (formulário) fixo no código faz todo sentido.
Em outros cenários onde a frequência de atualização dos formulários é grande ou seus campos são condicionados a outros fatores, faz mais sentido partir para uma solução dinâmica. Podemos por exemplo, ler uma lista de parâmetros de uma API e criar dinamicamente um formulário ou podemos mostrar e ocultar alguns campos do formulário dependendo do perfil do usuário. Há diversas aplicações, mas neste texto quero mostrar como consultar uma API e construir um formulário dinâmico utilizando o FormBuilder.array
do Angular.
Objetivo
Para deixar a leitura mais clara, o objetivo será criar um formulário com:
- um campo obrigatório:
nome
- um campo obrigatório para cada pergunta com duas opções de seleção:
Sim
ouNão
. As perguntas serão retornadas através de uma service de forma dinâmica, ou seja, se a lista de perguntas for alterada, o formulário deverá refletir a alteração.
Criando o formulário com um campo
Vamos começar do mais simples, um único FormGroup
com um único campo:
import { Component } from '@angular/core';
import {
FormBuilder,
FormGroup,
Validators,
} from '@angular/forms';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
nome: [null, [Validators.required]],
});
}
postarDados() {
const dados = this.form.value;
console.log(dados);
}
}
<form [formGroup]="form" (ngSubmit)="postarDados()">
<div>
<input
formControlName="nome"
placeholder="Nome..."
type="text" />
<small *ngIf="form.get('nome')?.errors">
Campo obrigatório
</small>
</div>
<div>
<input type="submit" value="Postar" />
</div>
</form>

Até aqui nenhuma novidade e tudo está bem simples. Porém, caso tenha dúvidas sugiro a leitura deste link antes de continuar com este texto.
Criando a service para retornar as perguntas do formulário
Antes de voltar ao formulário, vamos contextualizar como as perguntas que constarão no formulário serão consultadas.
Para simular o retorno de uma API criei no mocky.io a seguinte massa de dados:
[
{
"_id": 1,
"pergunta": "Pergunta 1"
},
{
"_id": 2,
"pergunta": "Pergunta 2"
},
{
"_id": 3,
"pergunta": "Pergunta 3"
}
]
A service para consultar este endpoint ficará da seguinte forma:
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Pergunta } from './pergunta.model';
@Injectable({ providedIn: 'root' })
export class PerguntaService {
// ****************************************
// Lembre-se de importar o HttpClientModule
// para utilizar o HttpClient.
// Neste exemplo fiz o import no arquivo
// AppModule
// ****************************************
constructor(private http: HttpClient) {}
consultarPerguntas(): Observable<Pergunta[]> {
const url =
'https://run.mocky.io/v3/5ede2fdb-32e2-4c65-a068-3906b61bc87d';
return this.http.get<Pergunta[]>(url);
}
}
export interface Pergunta {
_id: number;
pergunta: string;
}
Agora vamos injetar esta service no AppComponent
:
constructor(
private fb: FormBuilder,
private perguntaService: PerguntaService
) {
this.form = this.fb.group({
nome: [null, [Validators.required]],
});
this.perguntaService
.consultarPerguntas()
.subscribe((perguntas) => {
// TODO
});
}
Utilizando o FormBuilder.array
Até este ponto temos um formulário com um único campo, nome
, e um array de perguntas que o método consultarPerguntas()
devolve.
Como o objetivo é validar se o usuário respondeu todas as perguntas, precisamos de alguma forma criar estas perguntas dentro do form
, que por enquanto tem um único campo. Vamos por partes! primeiro pense de forma isolada, ou seja, somente em um FormGroup
para cada objeto Pergunta
. Então podemos escrever o seguinte:
criarFormGroupPergunta(pergunta: Pergunta): FormGroup {
return this.fb.group({
_id: [pergunta._id, [Validators.required]],
resposta: [null, [Validators.required]],
});
}
Como temos um array de perguntas, vamos ter um array de FormGroup
e uma forma de organizar este conjunto de FormGroup
dentro do form
é através do FormBuilder.array
:
this.form = this.fb.group({
nome: [null, [Validators.required]],
// Criando um FormArray:
perguntas: this.fb.array([
// Aqui vamos passar n FormGroup's
]),
});
AFormArray
aggregates the values of each childFormControl
into an array. It calculates its status by reducing the status values of its children. For example, if one of the controls in aFormArray
is invalid, the entire array becomes invalid.
fonte: https://angular.io/api/forms/FormArray#description
Por fim, vamos colocar estes trechos de código dentro do AppComponent
e efetuar um loop nas perguntas retornadas pelo PerguntaService
:
import { Component } from '@angular/core';
import {
FormArray,
FormBuilder,
FormGroup,
Validators,
} from '@angular/forms';
import { PerguntaService } from './perguntas/pergunta-service';
import { Pergunta } from './perguntas/pergunta.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
form: FormGroup;
constructor(
private fb: FormBuilder,
private perguntaService: PerguntaService
) {
this.form = this.fb.group({
nome: [null, [Validators.required]],
perguntas: this.fb.array([
// Aqui vamos passar n FormGroup's
]),
});
this.perguntaService
.consultarPerguntas()
.subscribe((perguntas) => {
console.log(perguntas);
// ***********************************
// Faz um loop para criar um FormGroup
// para cada pergunta.
// Cada FormGroup é adicionado no
// FormArray "perguntas"
// ***********************************
perguntas.forEach((pergunta) => {
const formGroupPergunta =
this.criarFormGroupPergunta(pergunta);
const formPerguntas = this.form.get(
'perguntas'
) as FormArray;
formPerguntas.push(formGroupPergunta);
});
});
}
postarDados() {
const dados = this.form.value;
console.log(dados);
}
criarFormGroupPergunta(pergunta: Pergunta): FormGroup {
return this.fb.group({
_id: [pergunta._id, [Validators.required]],
resposta: [null, [Validators.required]],
});
}
}
Acrescentando apenas a linha abaixo no template já podemos ver que o form.value
está um pouco diferente:
<!-- ...código ocultado... -->
{{ form.value | json }}
Na tela irá aparecer o seguinte conteúdo:

Renderizando o formulário de forma dinâmica
Agora precisamos fazer um loop em cima do perguntas
do form
para poder renderizar as perguntas e as opções de resposta:
<div
*ngFor="let pergunta of form.get('perguntas')?.controls">
TODO: Adicionar título da pergunta
TODO: Adicionar opções de seleção
</div>
Aqui temos um problema! Se você estiver utilizando o "strictTemplates": true
(arquivo tsconfig.json) vai pegar o seguinte erro:
error TS2339: Property 'controls' does not exist on type 'AbstractControl'.
O motivo do erro é que o form.get(...)
retorna um AbstractControl
. Como o AbstractControl
não tem um campo chamado controls
o erro aparece na tela.

Porém, sabemos que o resultado do nosso this.form.get('perguntas')
na realidade retorna um FormArray
, então temos que "dizer" isto de forma explícita. Para fazer isto vou criar um simples getter:
get formPerguntas(): FormArray {
return this.form.get('perguntas') as FormArray;
}
O getter será aproveitando para substituir os seguintes trechos:
// const formPerguntas = this.form.get(
// 'perguntas'
// ) as FormArray;
// formPerguntas.push(formGroupPergunta);
this.formPerguntas.push(formGroupPergunta);
<!-- ********************************************************
<div
*ngFor="let pergunta of form.get('perguntas')?.controls">
********************************************************* -->
<div
*ngFor="let pergunta of formPerguntas.controls">
TODO: Adicionar título da pergunta
TODO: Adicionar opções de seleção
</div>
Agora temos o resultado na tela:

Adicionando as opções para cada pergunta
Como temos um FormGroup
para cada pergunta, podemos escrever o seguinte:
<div *ngFor="let pergunta of formPerguntas.controls">
TODO: Adicionar título da pergunta
<ng-container [formGroup]="pergunta">
<input
type="radio"
formControlName="resposta"
[value]="false" />
Não
<input
type="radio"
formControlName="resposta"
[value]="true" />
Sim
</ng-container>
</div>
Novamente vamos pegar um outro erro com o "strictTemplates": true
:
TS2740: Type 'AbstractControl' is missing the following properties from type 'FormGroup': controls, registerControl, addControl, removeControl, and 3 more.
O motivo do erro é que o formPerguntas.controls
retorna um AbstractControl[]
e o AbstractControl
é uma classe abstrata que não implementa os métodos acima. Porém, a diretiva [formGroup]
espera um objeto do tipo FormGroup
que implementa os métodos abstratos do AbstractControl
. Como temos certeza de que o formPerguntas.controls
retorna um FormGroup[]
, podemos proceder de duas formas:
- efetuar um bypass nessa validação utilizando o
$any()
- criar um outro getter que faça a tipagem explícita - seguindo o modelo do
get formPerguntas(): FormArray
.
Vou optar pelo $any()
neste caso:
<form [formGroup]="form" (ngSubmit)="postarDados()">
<!-- NOME -->
<div>
<input
formControlName="nome"
placeholder="Nome..."
type="text" />
<small *ngIf="form.get('nome')?.errors">
Campo obrigatório
</small>
</div>
<!-- PERGUNTAS -->
<div *ngFor="let pergunta of formPerguntas.controls">
TODO: Adicionar título da pergunta
<ng-container [formGroup]="$any(pergunta)">
<input
type="radio"
formControlName="resposta"
[value]="false" />
Não
<input
type="radio"
formControlName="resposta"
[value]="true" />
Sim
</ng-container>
</div>
<div>
<input type="submit" value="Postar" />
</div>
</form>
{{ form.value | json }}
Até este ponto já temos o formulário funcionando corretamente:

O que está falando é o texto de cada pergunta. Uma das alternativas para obter o conteúdo da pergunta é guardar o array retornado pelo PerguntaService
em uma variável e criar uma lógica dentro do template utilizando o index
do *ngFor
. Seria algo do tipo:
<div
*ngFor="
let pergunta of formPerguntas.controls;
let i = index
">
{{ perguntas[i].pergunta }}
<!-- ... -->
</div<
Vou optar por um outro caminho, que julgo ser mais simples para este cenário. Vou adicionar o título da pergunta no próprio FormGroup
e manter este novo FormControl
desabilitado para que seu valor não seja incluído no form.value
:
criarFormGroupPergunta(pergunta: Pergunta): FormGroup {
return this.fb.group({
_id: [pergunta._id, [Validators.required]],
resposta: [null, [Validators.required]],
pergunta: {
value: pergunta.pergunta,
disabled: true,
},
});
}
No template:
<div *ngFor="let pergunta of formPerguntas.controls">
{{pergunta.get('pergunta')?.value}}
<!-- ...(código ocultado)... -->
</div>
O resultado final:

Código final:
<form [formGroup]="form" (ngSubmit)="postarDados()">
<!-- NOME -->
<div>
<input
formControlName="nome"
placeholder="Nome..."
type="text" />
<small *ngIf="form.get('nome')?.errors">
Campo obrigatório
</small>
</div>
<!-- PERGUNTAS -->
<div *ngFor="let pergunta of formPerguntas.controls">
{{pergunta.get('pergunta')?.value}}
<ng-container [formGroup]="$any(pergunta)">
<input
type="radio"
formControlName="resposta"
[value]="false" />
Não
<input
type="radio"
formControlName="resposta"
[value]="true" />
Sim
</ng-container>
</div>
<div>
<input type="submit" value="Postar" />
</div>
</form>
{{ form.value | json }}
import { Component } from '@angular/core';
import {
FormArray,
FormBuilder,
FormGroup,
Validators,
} from '@angular/forms';
import { PerguntaService } from './perguntas/pergunta-service';
import { Pergunta } from './perguntas/pergunta.model';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
form: FormGroup;
get formPerguntas(): FormArray {
return this.form.get('perguntas') as FormArray;
}
constructor(
private fb: FormBuilder,
private perguntaService: PerguntaService
) {
this.form = this.fb.group({
nome: [null, [Validators.required]],
perguntas: this.fb.array([
// Aqui vamos passar n FormGroup's
]),
});
this.perguntaService
.consultarPerguntas()
.subscribe((perguntas) => {
console.log(perguntas);
// ***********************************
// Faz um loop para criar um FormGroup
// para cada pergunta.
// Cada FormGroup é adicionado no
// FormArray "perguntas"
// ***********************************
perguntas.forEach((pergunta) => {
const formGroupPergunta =
this.criarFormGroupPergunta(pergunta);
this.formPerguntas.push(formGroupPergunta);
});
});
}
postarDados() {
const dados = this.form.value;
console.log(dados);
}
criarFormGroupPergunta(pergunta: Pergunta): FormGroup {
return this.fb.group({
_id: [pergunta._id, [Validators.required]],
resposta: [null, [Validators.required]],
pergunta: {
value: pergunta.pergunta,
disabled: true,
},
});
}
}
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { AppJsonPipe } from './app-json.pipe';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent, AppJsonPipe],
imports: [
AppRoutingModule,
BrowserModule,
HttpClientModule,
ReactiveFormsModule,
],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}
Considerações
Neste texto apresentei um formulário dinâmico bem simples, mas que pode servir de base para casos mais complexos. Podemos destacar os principais pontos sobre esta leitura:
- O
strictTemplates
realiza uma série de validações para ajudar o desenvolvedor a não cometer erros durante o tempo de compilação - O
FormGroup.value
ignora osFormControl
marcados comodisabled
- E é claro, como construir um formulário de forma dinâmica.
Links interessantes: