Внести вклад
Подробные руководства
Формы

Создание динамических форм

Многие формы, например анкеты, могут быть очень похожи друг на друга по формату и назначению. Чтобы быстрее и проще генерировать разные версии такой формы, можно создать шаблон динамической формы на основе метаданных, описывающих модель бизнес-объекта. Затем используйте шаблон для автоматической генерации новых форм в соответствии с изменениями модели данных.

Техника особенно полезна, когда есть тип формы, содержимое которой должно часто меняться, чтобы соответствовать быстро меняющимся бизнес- и регуляторным требованиям. Типичный сценарий — анкета. Может понадобиться получать ввод от пользователей в разных контекстах. Формат и стиль форм, которые видит пользователь, должны оставаться постоянными, а фактические вопросы, которые нужно задать, меняются с контекстом.

В этом туториале вы создадите динамическую форму, представляющую базовую анкету. Вы создаёте онлайн-заявку для героев, ищущих работу. Агентство постоянно меняет процесс подачи заявки, но используя динамическую форму, можно создавать новые формы на лету без изменения кода приложения.

Туториал проводит вас через следующие шаги:

  1. Включить reactive forms для проекта.
  2. Создать модель данных для представления form controls.
  3. Заполнить модель примерными данными.
  4. Разработать компонент для динамического создания form controls.

Создаваемая форма использует валидацию ввода и стилизацию для улучшения пользовательского опыта. У неё есть кнопка Submit, которая включается только когда весь пользовательский ввод валиден, и помечает невалидный ввод цветовой кодировкой и сообщениями об ошибках.

Базовая версия может развиваться для поддержки более богатого разнообразия вопросов, более изящного рендера и лучшего пользовательского опыта.

Включение reactive forms для проекта

Динамические формы основаны на reactive forms.

Чтобы дать приложению доступ к директивам reactive forms, импортируйте ReactiveFormsModule из пакета @angular/forms в необходимые компоненты.

Создание объектной модели формы

Динамической форме нужна объектная модель, которая может описать все сценарии, требуемые функциональностью формы. Пример формы заявки героя — набор вопросов: каждый control в форме должен задать вопрос и принять ответ.

Модель данных для такого типа формы должна представлять вопрос. Пример включает DynamicFormQuestionComponent, который определяет вопрос как фундаментальный объект в модели.

Следующий QuestionBase — базовый класс для набора controls, которые могут представлять вопрос и его ответ в форме.

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 || [];
  }
}

Определение классов controls

От этой базы пример выводит два новых класса — TextboxQuestion и DropdownQuestion — представляющих разные типы controls. Когда вы создадите шаблон формы на следующем шаге, вы создадите экземпляры этих конкретных типов вопросов, чтобы динамически рендерить соответствующие controls.

Тип control TextboxQuestion представляется в шаблоне формы элементом <input>. Он представляет вопрос и позволяет пользователям вводить данные. Атрибут type элемента определяется на основе поля type, указанного в аргументе options (например, text, email, url).

question-textbox.ts

import {QuestionBase} from './question-base';

export class TextboxQuestion extends QuestionBase<string> {
  override controlType = 'textbox';
}

Тип control DropdownQuestion представляет список вариантов в select box.

question-dropdown.ts

import {QuestionBase} from './question-base';

export class DropdownQuestion extends QuestionBase<string> {
  override controlType = 'dropdown';
}

Композиция групп форм

Динамическая форма использует сервис для создания сгруппированных наборов input controls на основе модели формы. Следующий QuestionControlService собирает набор экземпляров FormGroup, которые потребляют метаданные из модели вопроса. Можно указать значения по умолчанию и правила валидации.

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);
  }
}

Композиция содержимого динамической формы

Сама динамическая форма представлена компонентом-контейнером, который вы добавите на более позднем шаге. Каждый вопрос представляется в шаблоне компонента формы тегом <app-question>, который соответствует экземпляру DynamicFormQuestionComponent.

DynamicFormQuestionComponent отвечает за рендер деталей отдельного вопроса на основе значений в data-bound объекте вопроса. Форма опирается на директиву [formGroup] для связи HTML шаблона с underlying объектами controls. DynamicFormQuestionComponent создаёт группы форм и заполняет их controls, определёнными в модели вопроса, указывая правила отображения и валидации.

Цель DynamicFormQuestionComponent — представлять типы вопросов, определённые в вашей модели. Сейчас у вас только два типа вопросов, но можно представить гораздо больше. Блок @switch в шаблоне определяет, какой тип вопроса отображать. Switch использует директивы с селекторами formControlName и formGroup. Обе директивы определены в ReactiveFormsModule.

Поставка данных

Нужен ещё один сервис, чтобы поставлять конкретный набор вопросов, из которых строится отдельная форма. Для этого упражнения вы создаёте QuestionService для поставки этого массива вопросов из захардкоженных примерных данных. В реальном приложении сервис мог бы загружать данные из backend-системы. Ключевой момент, однако, в том, что вы полностью контролируете вопросы заявки героя через объекты, возвращаемые из QuestionService. Чтобы поддерживать анкету при изменении требований, нужно только добавлять, обновлять и удалять объекты из массива questions.

QuestionService поставляет набор вопросов в виде массива, привязанного к 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));
  }
}

Создание шаблона динамической формы

Компонент DynamicFormComponent — точка входа и основной контейнер формы, представленный как <app-dynamic-form> в шаблоне.

Компонент DynamicFormComponent представляет список вопросов, привязывая каждый к элементу <app-question>, который соответствует DynamicFormQuestionComponent.

Отображение формы

Чтобы отобразить экземпляр динамической формы, shell-шаблон AppComponent передаёт массив questions, возвращённый QuestionService, компоненту-контейнеру формы <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();
}

Это разделение модели и данных позволяет переиспользовать компоненты для любого типа опроса, пока он совместим с объектной моделью вопроса.

Обеспечение валидных данных

Шаблон формы использует динамическую привязку данных метаданных для рендера формы без каких-либо захардкоженных предположений о конкретных вопросах. Он динамически добавляет и метаданные controls, и критерии валидации.

Чтобы обеспечить валидный ввод, кнопка Save отключена, пока форма не в валидном состоянии. Когда форма валидна, нажмите Save, и приложение отрендерит текущие значения формы как JSON.

Следующий рисунок показывает финальную форму.

Dynamic-Form

Следующие шаги