Muchos formularios, como los cuestionarios, pueden ser muy similares entre sí en formato y propósito. Para hacer que sea más rápido y fácil generar diferentes versiones de dicho formulario, puedes crear una plantilla de formulario dinámico basada en metadatos que describen el modelo de objeto de negocio. Luego, utiliza la plantilla para generar nuevos formularios automáticamente, de acuerdo con los cambios en el modelo de datos.
La técnica es particularmente útil cuando tienes un tipo de formulario cuyo contenido debe cambiar frecuentemente para cumplir con requisitos de negocio y regulatorios que cambian rápidamente. Un caso de uso típico es un cuestionario. Podrías necesitar obtener entrada de usuarios en diferentes contextos. El formato y estilo de los formularios que ve un usuario deben permanecer constantes, mientras que las preguntas reales que necesitas hacer varían con el contexto.
En este tutorial construirás un formulario dinámico que presenta un cuestionario básico. Construyes una aplicación en línea para héroes que buscan empleo. La agencia está constantemente ajustando el proceso de aplicación, pero usando el formulario dinámico puedes crear los nuevos formularios sobre la marcha sin cambiar el código de la aplicación.
El tutorial te guía a través de los siguientes pasos.
- Habilitar formularios reactivos para un proyecto.
- Establecer un modelo de datos para representar controles de formulario.
- Poblar el modelo con datos de muestra.
- Desarrollar un componente para crear controles de formulario dinámicamente.
El formulario que creas utiliza validación de entrada y estilos para mejorar la experiencia del usuario. Tiene un botón de envío que solo se habilita cuando toda la entrada del usuario es válida, y marca la entrada no válida con codificación de colores y mensajes de error.
La versión básica puede evolucionar para admitir una variedad más amplia de preguntas, un renderizado más elegante y una mejor experiencia de usuario.
Habilitar formularios reactivos para tu proyecto
Los formularios dinámicos se basan en formularios reactivos.
Para que la aplicación tenga acceso a las directivas de formularios reactivos, importa ReactiveFormsModule
de la biblioteca @angular/forms
en los componentes necesarios.
dynamic-form.component.ts
import {Component, 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); questions = input<QuestionBase<string>[] | null>([]); form: 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 { question = input.required<QuestionBase<string>>(); form = input.required<FormGroup>(); get isValid() { return this.form().controls[this.question().key].valid; }}
Crea un modelo de objeto de formulario
Un formulario dinámico requiere un modelo de objeto que pueda describir todos los escenarios necesarios para la funcionalidad del formulario. El formulario de solicitud de héroe de ejemplo es un conjunto de preguntas, es decir, cada control en el formulario debe hacer una pregunta y aceptar una respuesta.
El modelo de datos para este tipo de formulario debe representar una pregunta.
El ejemplo incluye el DynamicFormQuestionComponent
, que define una pregunta como el objeto base del modelo.
El siguiente QuestionBase
es una clase base para un conjunto de controles que pueden representar la pregunta y su respuesta en el formulario.
src/app/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 || []; }}
Define clases de control
Desde esta base, el ejemplo deriva dos nuevas clases, TextboxQuestion
y DropdownQuestion
, que representan diferentes tipos de control.
Cuando creas la plantilla del formulario en el siguiente paso, instancias estos tipos específicos de pregunta para renderizar los controles apropiados dinámicamente.
El tipo de control TextboxQuestion
se representa en una plantilla de formulario usando un elemento <input>
. Presenta una pregunta y permite a los usuarios introducir datos. El atributo type
del elemento se define basado en el campo type
especificado en el argumento options
(por ejemplo text
, email
, url
).
question-textbox.ts
import {QuestionBase} from './question-base';export class TextboxQuestion extends QuestionBase<string> { override controlType = 'textbox';}
El tipo de control DropdownQuestion
presenta una lista de opciones en una caja de selección.
question-dropdown.ts
import {QuestionBase} from './question-base';export class DropdownQuestion extends QuestionBase<string> { override controlType = 'dropdown';}
Compón grupos de formulario
Un formulario dinámico usa un servicio para crear conjuntos agrupados de controles de entrada, basado en el modelo del formulario.
El siguiente QuestionControlService
recopila un conjunto de instancias de FormGroup
que consumen los metadatos del modelo de preguntas.
Puedes especificar valores por defecto y reglas de validación.
src/app/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); }}
Compón el contenido del formulario dinámico
El formulario dinámico se representa mediante un componente contenedor, que se agrega en un paso posterior.
Cada pregunta está representada en la plantilla del componente del formulario por una etiqueta <app-question>
, que coincide con una instancia de DynamicFormQuestionComponent
.
El DynamicFormQuestionComponent
es responsable de renderizar los detalles de una pregunta individual basado en valores en el objeto de pregunta vinculado a datos.
El formulario se basa en una directiva [formGroup]
para conectar el HTML de la plantilla a los objetos de control subyacentes.
El DynamicFormQuestionComponent
crea grupos de formulario y los puebla con controles definidos en el modelo de pregunta, especificando reglas de visualización y validación.
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 { question = input.required<QuestionBase<string>>(); form = input.required<FormGroup>(); get isValid() { return this.form().controls[this.question().key].valid; }}
El objetivo del DynamicFormQuestionComponent
es presentar tipos de pregunta definidos en tu modelo.
Solo tienes dos tipos de preguntas en este punto pero puedes imaginar muchas más.
El bloque @switch
en la plantilla determina qué tipo de pregunta se debe mostrar.
El switch usa directivas con los selectores formControlName
y formGroup
.
Ambas directivas están definidas en ReactiveFormsModule
.
Proporcionar datos
Se requiere otro servicio para proporcionar un conjunto específico de preguntas a partir del cual se construirá un formulario individual.
Para este ejercicio creas el QuestionService
para proporcionar este array de preguntas desde los datos de muestra codificados.
En una aplicación del mundo real, el servicio podría obtener datos de un sistema backend.
El punto clave, sin embargo, es que controlas las preguntas de aplicación de trabajo de héroe completamente a través de los objetos devueltos de QuestionService
.
Para mantener el cuestionario a medida que cambian los requisitos, solo necesitas agregar, actualizar y eliminar objetos del array questions
.
El QuestionService
proporciona un conjunto de preguntas en forma de un array vinculado a input()
questions.
src/app/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)); }}
Crea una plantilla de formulario dinámico
El componente DynamicFormComponent
es el punto de entrada y el contenedor principal para el formulario, que se representa usando <app-dynamic-form>
en una plantilla.
El componente DynamicFormComponent
presenta una lista de preguntas vinculando cada una a un elemento <app-question>
que coincide con el 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, 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); questions = input<QuestionBase<string>[] | null>([]); form: FormGroup = this.qcs.toFormGroup(this.questions() as QuestionBase<string>[]); payLoad = ''; onSubmit() { this.payLoad = JSON.stringify(this.form.getRawValue()); }}
Muestra el formulario
Para mostrar una instancia del formulario dinámico, la plantilla shell AppComponent
pasa el array questions
devuelto por el QuestionService
al componente contenedor del formulario, <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 separación de modelo y datos te permite reutilizar los componentes para cualquier tipo de encuesta, siempre que sea compatible con el modelo de objeto question.
Asegurando datos válidos
La plantilla del formulario usa vinculación dinámica de datos de metadatos para renderizar el formulario sin hacer suposiciones codificadas sobre preguntas específicas. Agrega tanto metadatos de control como criterios de validación dinámicamente.
Para garantizar que la entrada sea válida, el botón Save está deshabilitado hasta que el formulario está en un estado válido. Cuando el formulario es válido, haz clic en Save y la aplicación renderiza los valores actuales del formulario como JSON.
La siguiente figura muestra el formulario final.
