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 ou Nã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:
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:
A service para consultar este endpoint ficará da seguinte forma:
Agora vamos injetar esta service no AppComponent:
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:
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:
A FormArray aggregates the values of each child FormControl into an array. It calculates its status by reducing the status values of its children. For example, if one of the controls in a FormArray is invalid, the entire array becomes invalid.
Por fim, vamos colocar estes trechos de código dentro do AppComponent e efetuar um loop nas perguntas retornadas pelo PerguntaService:
Acrescentando apenas a linha abaixo no template já podemos ver que o form.value está um pouco diferente:
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:
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:
O getter será aproveitando para substituir os seguintes trechos:
Agora temos o resultado na tela:
Adicionando as opções para cada pergunta
Como temos um FormGroup para cada pergunta, podemos escrever o seguinte:
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:
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:
No template:
O resultado final:
Código final:
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 os FormControl marcados como disabled
E é claro, como construir um formulário de forma dinâmica.