Muitos formulários, como questionários, podem ser muito semelhantes entre si em formato e intenção. Para tornar mais rápido e fácil gerar diferentes versões de tal formulário, você pode criar um template de formulário dinâmico baseado em metadados que descrevem o modelo de objeto de negócio. Em seguida, use o template para gerar novos formulários automaticamente, de acordo com as mudanças no modelo de dados.
A técnica é particularmente útil quando você tem um tipo de formulário cujo conteúdo deve mudar frequentemente para atender a requisitos de negócio e regulatórios em rápida mudança. Um caso de uso típico é um questionário. Você pode precisar obter entrada de usuários em diferentes contextos. O formato e o estilo dos formulários que um usuário vê devem permanecer constantes, enquanto as perguntas reais que você precisa fazer variam com o contexto.
Neste tutorial você construirá um formulário dinâmico que apresenta um questionário básico. Você constrói um aplicativo online para heróis que buscam emprego. A agência está constantemente ajustando o processo de inscrição, mas usando o formulário dinâmico você pode criar os novos formulários dinamicamente sem alterar o código do aplicativo.
O tutorial orienta você através das seguintes etapas.
- Habilitar reactive forms para um projeto.
- Estabelecer um modelo de dados para representar controles de formulário.
- Preencher o modelo com dados de exemplo.
- Desenvolver um component para criar controles de formulário dinamicamente.
O formulário que você cria usa validação de entrada e estilização para melhorar a experiência do usuário. Ele tem um botão Submit que só é habilitado quando toda a entrada do usuário é válida, e sinaliza entrada inválida com codificação de cores e mensagens de erro.
A versão básica pode evoluir para suportar uma variedade mais rica de perguntas, renderização mais elegante e experiência de usuário superior.
Habilitar reactive forms para seu projeto
Formulários dinâmicos são baseados em reactive forms.
Para dar ao aplicativo acesso às directives de reactive forms, importe ReactiveFormsModule da biblioteca @angular/forms nos components necessários.
dynamic-form.component.ts
import {Component, computed, inject, input} from '@angular/core';import {FormGroup, ReactiveFormsModule} from '@angular/forms';import {DynamicFormQuestionComponent} from './dynamic-form-question.component';import {QuestionBase} from './question-base';import {QuestionControlService} from './question-control.service';@Component({ selector: 'app-dynamic-form', templateUrl: './dynamic-form.component.html', providers: [QuestionControlService], imports: [DynamicFormQuestionComponent, ReactiveFormsModule],})export class DynamicFormComponent { private readonly qcs = inject(QuestionControlService); readonly questions = input<QuestionBase<string>[] | null>([]); readonly form = computed<FormGroup>(() => this.qcs.toFormGroup(this.questions() as QuestionBase<string>[]), ); payLoad = ''; onSubmit() { this.payLoad = JSON.stringify(this.form().getRawValue()); }}
dynamic-form-question.component.ts
import {Component, input, Input} from '@angular/core';import {FormGroup, ReactiveFormsModule} from '@angular/forms';import {QuestionBase} from './question-base';@Component({ selector: 'app-question', templateUrl: './dynamic-form-question.component.html', imports: [ReactiveFormsModule],})export class DynamicFormQuestionComponent { readonly question = input.required<QuestionBase<string>>(); readonly form = input.required<FormGroup>(); get isValid() { return this.form().controls[this.question().key].valid; }}
Criar um modelo de objeto de formulário
Um formulário dinâmico requer um modelo de objeto que possa descrever todos os cenários necessários pela funcionalidade do formulário. O formulário de exemplo de aplicação de herói é um conjunto de perguntas — isto é, cada controle no formulário deve fazer uma pergunta e aceitar uma resposta.
O modelo de dados para este tipo de formulário deve representar uma pergunta.
O exemplo inclui o DynamicFormQuestionComponent, que define uma pergunta como o objeto fundamental no modelo.
A seguinte QuestionBase é uma classe base para um conjunto de controles que podem representar a pergunta e sua resposta no formulário.
question-base.ts
export class QuestionBase<T> { value: T | undefined; key: string; label: string; required: boolean; order: number; controlType: string; type: string; options: {key: string; value: string}[]; constructor( options: { value?: T; key?: string; label?: string; required?: boolean; 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.order = options.order === undefined ? 1 : options.order; this.controlType = options.controlType || ''; this.type = options.type || ''; this.options = options.options || []; }}
Definir classes de controle
A partir desta base, o exemplo deriva duas novas classes, TextboxQuestion e DropdownQuestion, que representam diferentes tipos de controle.
Quando você criar o template do formulário no próximo passo, você instanciará esses tipos de pergunta específicos para renderizar os controles apropriados dinamicamente.
O tipo de controle TextboxQuestion é representado em um template de formulário usando um elemento <input>. Ele apresenta uma pergunta e permite que os usuários insiram entrada. O atributo type do elemento é definido com base no campo type especificado no argumento options (por exemplo text, email, url).
question-textbox.ts
import {QuestionBase} from './question-base';export class TextboxQuestion extends QuestionBase<string> { override controlType = 'textbox';}
O tipo de controle DropdownQuestion apresenta uma lista de escolhas em uma caixa de seleção.
question-dropdown.ts
import {QuestionBase} from './question-base';export class DropdownQuestion extends QuestionBase<string> { override controlType = 'dropdown';}
Compor grupos de formulário
Um formulário dinâmico usa um service para criar conjuntos agrupados de controles de entrada, baseados no modelo de formulário.
O seguinte QuestionControlService coleta um conjunto de instâncias FormGroup que consomem os metadados do modelo de pergunta.
Você pode especificar valores padrão e regras de validação.
question-control.service.ts
import {Injectable} from '@angular/core';import {FormControl, FormGroup, Validators} from '@angular/forms';import {QuestionBase} from './question-base';@Injectable()export class QuestionControlService { toFormGroup(questions: QuestionBase<string>[]) { const group: any = {}; questions.forEach((question) => { group[question.key] = question.required ? new FormControl(question.value || '', Validators.required) : new FormControl(question.value || ''); }); return new FormGroup(group); }}
Compor conteúdo de formulário dinâmico
O formulário dinâmico em si é representado por um component contêiner, que você adiciona em uma etapa posterior.
Cada pergunta é representada no template do component do formulário por uma tag <app-question>, que corresponde a uma instância de DynamicFormQuestionComponent.
O DynamicFormQuestionComponent é responsável por renderizar os detalhes de uma pergunta individual com base nos valores no objeto de pergunta vinculado aos dados.
O formulário depende de uma directive [formGroup] para conectar o HTML do template aos objetos de controle subjacentes.
O DynamicFormQuestionComponent cria grupos de formulário e os preenche com controles definidos no modelo de pergunta, especificando regras de exibição e validação.
dynamic-form-question.component.html
<div [formGroup]="form()"> <label [attr.for]="question().key">{{ question().label }}</label> <div> @switch (question().controlType) { @case ('textbox') { <input [formControlName]="question().key" [id]="question().key" [type]="question().type" /> } @case ('dropdown') { <select [id]="question().key" [formControlName]="question().key"> @for (opt of question().options; track opt) { <option [value]="opt.key">{{ opt.value }}</option> } </select> } } </div> @if (!isValid) { <div class="errorMessage">{{ question().label }} is required</div> }</div>
dynamic-form-question.component.ts
import {Component, input, Input} from '@angular/core';import {FormGroup, ReactiveFormsModule} from '@angular/forms';import {QuestionBase} from './question-base';@Component({ selector: 'app-question', templateUrl: './dynamic-form-question.component.html', imports: [ReactiveFormsModule],})export class DynamicFormQuestionComponent { readonly question = input.required<QuestionBase<string>>(); readonly form = input.required<FormGroup>(); get isValid() { return this.form().controls[this.question().key].valid; }}
O objetivo do DynamicFormQuestionComponent é apresentar tipos de pergunta definidos em seu modelo.
Você tem apenas dois tipos de perguntas neste ponto, mas pode imaginar muitos mais.
O bloco @switch no template determina qual tipo de pergunta exibir.
O switch usa directives com os seletores formControlName e formGroup.
Ambas as directives são definidas em ReactiveFormsModule.
Fornecer dados
Outro service é necessário para fornecer um conjunto específico de perguntas a partir das quais construir um formulário individual.
Para este exercício, você cria o QuestionService para fornecer este array de perguntas a partir dos dados de exemplo codificados.
Em um aplicativo do mundo real, o service pode buscar dados de um sistema backend.
O ponto chave, no entanto, é que você controla as perguntas de inscrição de emprego de herói inteiramente através dos objetos retornados de QuestionService.
Para manter o questionário conforme os requisitos mudam, você só precisa adicionar, atualizar e remover objetos do array questions.
O QuestionService fornece um conjunto de perguntas na forma de um array vinculado ao input() questions.
question.service.ts
import {Injectable} from '@angular/core';import {DropdownQuestion} from './question-dropdown';import {QuestionBase} from './question-base';import {TextboxQuestion} from './question-textbox';import {of} from 'rxjs';@Injectable()export class QuestionService { // TODO: get from a remote source of question metadata getQuestions() { const questions: QuestionBase<string>[] = [ new DropdownQuestion({ key: 'favoriteAnimal', label: 'Favorite Animal', options: [ {key: 'cat', value: 'Cat'}, {key: 'dog', value: 'Dog'}, {key: 'horse', value: 'Horse'}, {key: 'capybara', value: 'Capybara'}, ], order: 3, }), new TextboxQuestion({ key: 'firstName', label: 'First name', value: 'Alex', required: true, order: 1, }), new TextboxQuestion({ key: 'emailAddress', label: 'Email', type: 'email', order: 2, }), ]; return of(questions.sort((a, b) => a.order - b.order)); }}
Criar um template de formulário dinâmico
O component DynamicFormComponent é o ponto de entrada e o contêiner principal para o formulário, que é representado usando <app-dynamic-form> em um template.
O component DynamicFormComponent apresenta uma lista de perguntas vinculando cada uma a um elemento <app-question> que corresponde ao DynamicFormQuestionComponent.
dynamic-form.component.html
<div> <form (ngSubmit)="onSubmit()" [formGroup]="form()"> @for (question of questions(); track question) { <div class="form-row"> <app-question [question]="question" [form]="form()" /> </div> } <div class="form-row"> <button type="submit" [disabled]="!form().valid">Save</button> </div> </form> @if (payLoad) { <div class="form-row"><strong>Saved the following values</strong><br />{{ payLoad }}</div> }</div>
dynamic-form.component.ts
import {Component, computed, inject, input} from '@angular/core';import {FormGroup, ReactiveFormsModule} from '@angular/forms';import {DynamicFormQuestionComponent} from './dynamic-form-question.component';import {QuestionBase} from './question-base';import {QuestionControlService} from './question-control.service';@Component({ selector: 'app-dynamic-form', templateUrl: './dynamic-form.component.html', providers: [QuestionControlService], imports: [DynamicFormQuestionComponent, ReactiveFormsModule],})export class DynamicFormComponent { private readonly qcs = inject(QuestionControlService); readonly questions = input<QuestionBase<string>[] | null>([]); readonly form = computed<FormGroup>(() => this.qcs.toFormGroup(this.questions() as QuestionBase<string>[]), ); payLoad = ''; onSubmit() { this.payLoad = JSON.stringify(this.form().getRawValue()); }}
Exibir o formulário
Para exibir uma instância do formulário dinâmico, o template shell do AppComponent passa o array questions retornado pelo QuestionService para o component contêiner do formulário, <app-dynamic-form>.
app.component.ts
import {Component, inject} from '@angular/core';import {AsyncPipe} from '@angular/common';import {DynamicFormComponent} from './dynamic-form.component';import {QuestionService} from './question.service';import {QuestionBase} from './question-base';import {Observable} from 'rxjs';@Component({ selector: 'app-root', template: ` <div> <h2>Job Application for Heroes</h2> <app-dynamic-form [questions]="questions$ | async" /> </div> `, providers: [QuestionService], imports: [AsyncPipe, DynamicFormComponent],})export class AppComponent { questions$: Observable<QuestionBase<string>[]> = inject(QuestionService).getQuestions();}
Esta separação de modelo e dados permite reutilizar os components para qualquer tipo de pesquisa, desde que seja compatível com o modelo de objeto question.
Garantir dados válidos
O template do formulário usa vinculação de dados dinâmica de metadados para renderizar o formulário sem fazer suposições codificadas sobre perguntas específicas. Ele adiciona tanto metadados de controle quanto critérios de validação dinamicamente.
Para garantir entrada válida, o botão Save é desabilitado até que o formulário esteja em um estado válido. Quando o formulário está válido, clique em Save e o aplicativo renderiza os valores atuais do formulário como JSON.
A figura a seguir mostra o formulário final.