Como criar um formulário dinâmico avançado com Angular?


Precisando de um guia avançado para criar um formulário dinâmico com Angular?

Esse é o tutorial passo a passo que você está precisando.

Tabela de Conteúdo 👇

Uma história comum

Porque essa situação é muito recorrente?

O projeto pelo qual você está suando está finalmente se tornando um sucesso...

...e você está quase acabando...

Mas na última hora, aquele formulário precisa de uma mudança, e essa mudança precisa acontecer AGORA!

Suspiro.

Esse tipo de situação é muito comum. Então o que você acha de um método proativo que nos permita renderizar dinamicamente um formulário Angular? E fazer com que nosso formulário lide com uma série de situações difíceis?

Que tal criar um formulário dinâmico com Angular para que nós possamos encaminhar o blob do JSON ou outra coisa que ele possa renderizar?

Boa ideia, não é?

Mas... Espera um pouco. O que nós precisamos para renderizar dinamicamente? Temos várias coisas para resolver, por exemplo:

  • Texto de entrada
  • Área de texto
  • Botões
  • Caixas de check
  • Validação do formulário
  • E muito mais!

Caramba! Existe alguma forma de fazer isso de sofisticadamente?

Nesse artigo eu vou guiar você passo a passo para construir um formulário dinâmico como a imagem mostra a seguir. Esse exemplo utiliza CSS Bootstrap para o estilo do formulário, mas você pode personalizar da forma que você desejar.

Esse exemplo utiliza CSS Bootstrap para o estilo do formulário, mas você pode personalizar da forma que você desejar.

Gostaria de saber o código todo agora? Esse é o link.

Criando modelo de dados

O primeiro passo é criar um modelo para os campos de entrada. Vamos criar um arquivo chamado form-field.ts e exportar nosso modelo, assim:

export class FormField<T> {
  value: T;
  key: string;
  label: string;
  required: boolean;
  validator: string;
  order: number;
  controlType: string;
  type: string;
  options: { key: string; value: string }[];

  constructor(
    options: {
      value?: T;
      key?: string;
      label?: string;
      required?: boolean;
      validator?: string;
      order?: number;
      controlType?: string;
      type?: string;
      options?: { key: string; value: string }[];
    } = {}
  ) {
    this.value = options.value;
    this.key = options.key || "";
    this.label = options.label || "";
    this.required = !!options.required;
    this.validator = options.validator || "";
    this.order = options.order === undefined ? 1 : options.order;
    this.controlType = options.controlType || "";
    this.type = options.type || "";
    this.options = options.options || [];
  }
}

Criando o serviço de dados

O próximo passo é criar um serviço que vai pegar nossos dados e retornar para o grupo de formulário.

Vamos pegar o Angular CLI e gerar o serviço usando o seguinte comando:

Depois inserir alguns poderes do formulário dinâmico com o seguinte código:

import { Injectable } from '@angular/core';
import { FormControl, FormGroup, ValidatorFn, Validators } from '@angular/forms';
import { of } from 'rxjs';
import { FormField } from './form-field';

@Injectable({
  providedIn: 'root'
})
export class FormfieldControlService {

  constructor() { }

  toFormGroup(inputs: FormField<string>[]): FormGroup {
    const group: any = {};
    inputs.forEach(input => {
      let validator: ValidatorFn[] = input.required ? [Validators.required] : [];
      switch (input.validator) {
        case "email":
          validator.push(Validators.email);
          break;
        default:
          break;
      }
      group[input.key] = validator.length > 0 ? new FormControl(input.value || '', validator)
                                        : new FormControl(input.value || '');
    });

    return new FormGroup(group);
  }

  getFormFields() {

    const inputs: FormField<string>[] = [

      new FormField<string>({
        controlType: "textbox",
        key: 'name',
        label: 'Name',
        required: true,
        order: 1
      }),

      new FormField<string>({
        controlType: "textbox",
        key: 'email',
        label: 'Email',
        type: 'email',
        required: true,
        validator: "email",
        order: 2
      }),

      new FormField<string>({
        controlType: "dropdown",
        key: 'country',
        label: 'Country',
        options: [
          {key: 'usa',  value: 'United States of America'},
          {key: 'br',  value: 'Brazil'},
          {key: 'other',   value: 'Other'}
        ],
        order: 3
      }),

      new FormField<string>({
        controlType: "checkbox",
        key: 'agree',
        label: 'I accept terms and services',
        type: 'checkbox',
        required: true,
        order: 4
      }),

      new FormField<string>({
        controlType: "radio",
        key: 'sex',
        label: 'Sex',
        type: 'radio',
        options: [
          {key: 'male',  value: 'Male'},
          {key: 'female',  value: 'Female'}
        ],
        order: 5
      }),

      new FormField<string>({
        controlType: "textarea",
        key: 'message',
        label: 'Mesage',
        type: 'textarea',
        order: 6
      })
    ];

    return of(inputs.sort((a, b) => a.order - b.order));
  }

}

Muito difícil? Complicado?

Ótimo. Isso quer dizer que você está aprendendo coisas novas!

Note que nesse serviço nós temos duas funções. Uma delas pega o grupo de FormField e devolve para o FormGroup. A segunda função devolve o grupo de FormField. É na segunda opção que você pode usar sua imaginação para pegar os dados do formulário para qualquer objetivo que você queira.

Crie o componente de entrada do formulário

O próximo passo é criar os componentes que vão renderizar seu formulário dinâmico.

Vamos criar o primeiro e intitular como dynamic-form-input. Esse é o comando Angular CLI:

ng generate component dynamic-form-input

Vamos editar o dynamic-form-input.component.ts para tomar o FormGroup e FormField<string> com um Input.

import { Component, Input, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
import { FormField } from '../form-field';

@Component({
  selector: 'app-dynamic-form-input',
  templateUrl: './dynamic-form-input.component.html',
  styleUrls: ['./dynamic-form-input.component.css']
})
export class DynamicFormInputComponent {

  @Input() input: FormField<string>;
  @Input() form: FormGroup;

  get isValid() { return this.form.controls[this.input.key].valid; }

}

E por final editar o dynamic-form-input.component.html para renderizar o campo de formulário.

<div [formGroup]="form" class="form-group">

    <div [ngSwitch]="input.controlType">

      <div *ngSwitchCase="'textbox'">
        <label [attr.for]="input.key">{{input.label}}</label>
        <input class="form-control" [formControlName]="input.key" [id]="input.key" [type]="input.type">
      </div>

      <div *ngSwitchCase="'dropdown'">
        <label [attr.for]="input.key">{{input.label}}</label>
        <select class="custom-select" [id]="input.key" [formControlName]="input.key">
          <option *ngFor="let opt of input.options" [value]="opt.key">{{opt.value}}</option>
        </select>
      </div>

      <div *ngSwitchCase="'checkbox'">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" [formControlName]="input.key" [id]="input.key">
          <label class="form-check-label" [attr.for]="input.key">{{input.label}}</label>
        </div>
      </div>

      <div *ngSwitchCase="'radio'">
        <div class="form-check form-check-inline" *ngFor="let opt of input.options">
          <input class="form-check-input" type="radio" [formControlName]="input.key" [id]="input.key" [value]="opt.value">
          <label class="form-check-label" [attr.for]="opt.key"> {{ opt.value }} </label>
        </div>
      </div>

      <div *ngSwitchCase="'textarea'">
        <label [attr.for]="input.key">{{input.label}}</label>
        <textarea class="form-control" [formControlName]="input.key" [id]="input.key" rows="5"></textarea>
      </div>

    </div>

</div>

Crie o componente do formulário

E, por fim, vamos gerar nosso último componente.

ng generate component dynamic-form

Esse é o código para o arquivo .ts.

import { FormGroup } from '@angular/forms';
import { FormField } from '../form-field';
import { FormfieldControlService } from '../formfield-control.service';

@Component({
  selector: 'app-dynamic-form',
  templateUrl: './dynamic-form.component.html',
  styleUrls: ['./dynamic-form.component.css']
})
export class DynamicFormComponent implements OnInit {

  @Input() formFields: FormField<string>[] = [];
  form: FormGroup;
  payLoad = ' ';

  constructor(private formfieldService: FormfieldControlService) { }

  ngOnInit(): void {
    this.form = this.formfieldService.toFormGroup(this.formFields);
  }

  onSubmit() {
    this.payLoad = JSON.stringify(this.form.getRawValue());
  }

}

E o HTML.

<div>
  <form (ngSubmit)="onSubmit()" [formGroup]="form">

    <div *ngFor="let formField of formFields" class="form-group">
      <app-dynamic-form-input [input]="formField" [form]="form"></app-dynamic-form-input>
    </div>

    <div class="form-group">
      <button type="submit" class="btn btn-primary" [disabled]="form.invalid">Save</button>
    </div>
  </form>

  <div *ngIf="payLoad" class="form-group">
    <strong>Here's the payload</strong><br>{{payLoad}}
  </div>
</div>

Preenchendo nosso formulário

Agora temos tudo prontinho. Tudo o que precisamos fazer é "ligar".

Dentro do nosso arquivo app.component.ts vamos pegar os dados para o nosso formulário dinâmico.

import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { FormField } from './form-field';
import { FormfieldControlService } from './formfield-control.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [FormfieldControlService]
})
export class AppComponent {
  title = 'AngularDynamicForms';
  formFields: Observable<FormField<any>[]>;
  constructor(service: FormfieldControlService) {
    this.formFields = service.getFormFields();
  }
}

Então em app.component.html vamos fazer o template do formulário dinâmico e passar para os dados dinâmicos.

<app-dynamic-form [formFields]="formFields | async "></app-dynamic-form>

Está feito!!! É isso!

Conclusão

Esse é um exemplo avançado e bem explicado de como um formulário dinâmico com Angular pode ser criado.

Nós vimos um modelo para moldar diferentes campos de formulário e então criamos um serviço para os campos de formulário e por fim, a renderização do formulário dinâmico em um componente.

Muito trabalhoso? Esse é um a solução complexa para um simples formulário de contato, mas quando são muitos requerimentos, vale a pena pensar em utilizar o formulário dinâmico com Angular.

Dúvidas ou comentários? Esqueci de mencionar alguma coisa? Não deixe de entrar em contato.

Leitura Adicional