Angular 2 - обработка форм больших масштабов

В компании Im, работающей для, разрабатывалось крупномасштабное приложение с несколькими формами, которое пользователь должен заполнить, чтобы зарегистрироваться для нашей программы. Когда на все вопросы был дан ответ, пользователь достигает раздела, в котором суммируются все их ответы, высвечиваются недействительные ответы и дает пользователю возможность вернуться к предыдущим шагам формы и пересмотреть их ответы. Эта логика будет повторяться в разных разделах верхнего уровня, каждая из которых имеет несколько шагов/страниц и сводную страницу.

Для этого мы создали компонент для каждого отдельного шага формы (такие категории, как "Личные данные" или "Квалификации" и т.д.) вместе с их соответствующими маршрутами и компонентом для сводной страницы.

Чтобы сделать его максимально сухим, мы начали создавать "главную" службу, которая хранит информацию для всех разных шагов формы (значения, достоверность и т.д.).

import { Injectable } from '@angular/core';
import { Validators } from '@angular/forms';
import { ValidationService } from '../components/validation/index';

@Injectable()
export class FormControlsService {
  static getFormControls() {
    return [
      {
        name: 'personalDetailsForm$',
        groups: {
          name$: [
            {
              name: 'firstname$',
              validations: [
                Validators.required,
                Validators.minLength(2)
              ]
            },
            {
              name: 'lastname$',
              validations: [
                Validators.required,
                Validators.minLength(2)
              ]
            }
          ],
          gender$: [
            {
              name: 'gender$',
              validations: [
                Validators.required
              ]
            }
          ],
          address$: [
            {
              name: 'streetaddress$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'city$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'state$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'zip$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'country$',
              validations: [
                Validators.required
              ]
            }
          ],
          phone$: [
            {
              name: 'phone$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'countrycode$',
              validations: [
                Validators.required
              ]
            }
          ],
        }
      },
      {
        name: 'parentForm$',
        groups: {
          all: [
            {
              name: 'parentName$',
              validations: [
                Validators.required
              ]
            },
            {
              name: 'parentEmail$',
              validations: [
                ValidationService.emailValidator
              ]
            },
            {
              name: 'parentOccupation$'
            },
            {
              name: 'parentTelephone$'
            }
          ]
        }
      },
      {
        name: 'responsibilitiesForm$',
        groups: {
          all: [
            {
              name: 'hasDrivingLicense$',
              validations: [
                Validators.required,
              ]
            },
            {
              name: 'drivingMonth$',
              validations: [
                ValidationService.monthValidator
              ]
            },
            {
              name: 'drivingYear$',
              validations: [
                ValidationService.yearValidator
              ]
            },
            {
              name: 'driveTimesPerWeek$',
              validations: [
                Validators.required
              ]
            },
          ]
        }
      }
    ];
  }
}

Эта служба используется всеми компонентами для настройки привязок формы HTML для каждого, путем доступа к соответствующему объекту и создания вложенных групп форм, а также страницы сводки, уровень представления которых всего лишь один раз (Модель → Вид).

export class FormManagerService {
    mainForm: FormGroup;

    constructor(private fb: FormBuilder) {
    }

    setupFormControls() {
        let allForms = {};
        this.forms = FormControlsService.getFormControls();

        for (let form of this.forms) {

            let resultingForm = {};

            Object.keys(form['groups']).forEach(group => {

                let formGroup = {};
                for (let field of form['groups'][group]) {
                    formGroup[field.name] = ['', this.getFieldValidators(field)];
                }

                resultingForm[group] = this.fb.group(formGroup);
            });

            allForms[form.name] = this.fb.group(resultingForm);
        }

        this.mainForm = this.fb.group(allForms);
    }

    getFieldValidators(field): Validators[] {
        let result = [];

        for (let validation of field.validations) {
            result.push(validation);
        }

        return (result.length > 0) ? [Validators.compose(result)] : [];
    }
}

После этого мы начали использовать следующий синтаксис в компонентах, чтобы получить элементы управления формы, указанные в службе основной формы:

personalDetailsForm$: AbstractControl;
streetaddress$: AbstractControl;

constructor(private fm: FormManagerService) {
    this.personalDetailsForm$ = this.fm.mainForm.controls['personalDetailsForm$'];
    this.streetaddress$ = this.personalDetailsForm$['controls']['address$']['controls']['streetaddress$'];
}

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

Мы обсуждали разные решения, но мы не можем придумать тот, который использует механизм формы Angular s, позволяет нам сохранить нашу иерархию проверки целостности и также просто.

Есть ли лучший способ достичь того, что пытались сделать?

Ответ 1

Я прокомментировал в другом месте @ngrx/store, и, хотя я по-прежнему рекомендую его, я считаю, что я неправильно понял вашу проблему.

Во всяком случае, ваш FormsControlService в основном глобальный const. Серьезно замените export class FormControlService ... на

export const formControlsDefinitions = {
   // ...
};

и какая разница? Вместо того, чтобы получать услугу, вы просто импортируете объект. И так как мы сейчас рассматриваем его как типизированный глобальный const, мы можем определить интерфейсы, которые мы используем...

export interface ModelControl<T> {
    name: string;
    validators: ValidatorFn[];
}

export interface ModelGroup<T> {
   name: string;
   // Any subgroups of the group
   groups?: ModelGroup<any>[];
   // Any form controls of the group
   controls?: ModelControl<any>[];
}

и поскольку мы это сделали, мы можем перенести определения отдельных групп форм из одного монолитного модуля и определить группу форм, где мы определяем модель. Гораздо чище.

// personal_details.ts

export interface PersonalDetails {
  ...
}

export const personalDetailsFormGroup: ModelGroup<PersonalDetails> = {
   name: 'personalDetails$';
   groups: [...]
}

Но теперь у нас есть все эти индивидуальные определения групп форм, разбросанные по всем нашим модулям, и нет возможности собрать их все: (Нам нужно каким-то образом узнать все группы форм в нашем приложении.

Но мы не знаем, сколько модулей у нас будет в будущем, и мы можем захотеть их лениво загрузить, поэтому их группы моделей не могут быть зарегистрированы при запуске приложения.

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

export const MODEL_GROUP = new OpaqueToken('my_model_group');

/**
 * All the form controls for the application
 */
export class FormControlService {
    constructor(
        @Inject(MMODEL_GROUP) rootControls: ModelGroup<any>[]
    ) {}

    getControl(name: string): AbstractControl { /etc. }
}

затем создайте модуль манифеста где-нибудь (который вводится в модуль приложения "core" ), создавая форму FormService

@NgModule({
   providers : [
     {provide: MODEL_GROUP, useValue: personalDetailsFormGroup, multi: true}
     // and all your other form groups
     // finally inject our service, which knows about all the form controls
     // our app will ever use.
     FormControlService
   ]
})
export class CoreFormControlsModule {}

Теперь у нас есть решение, которое:

  • более локально, элементы формы объявляются вместе с моделями
  • более масштабируемый, просто нужно добавить элемент управления формой, а затем добавить его в модуль манифеста; и
  • меньше монолитных, нет классов конфигурации "god".

Ответ 2

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

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

Мы столкнулись с точно такой же проблемой, и после нескольких месяцев борьбы с огромными, вложенными и иногда полиморфными формами мы нашли решение, которое нас порадует, простое в использовании и которое дает нам "суперсилы" (типа безопасность как в TS, так и в HTML), доступ к вложенным ошибкам и другие.

Мы решили извлечь это в отдельную библиотеку и открыть ее.
Исходный код доступен здесь: https://github.com/cloudnc/ngx-sub-form
И пакет npm может быть установлен следующим образом: npm я ngx-sub-form

За кулисами наша библиотека использует ControlValueAccessor и это позволяет нам использовать его в шаблонных формах И реактивных формах (вы получите лучшее из этого, используя реактивные формы).

Так о чем это все?

Прежде чем я начну объяснять, если вы предпочитаете следовать за надлежащим редактором, я сделал пример Stackblitz: https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling

Ну, пример стоит 1000 слов, так что давайте переделаем одну часть вашей формы (самую сложную с вложенными данными): personalDetailsForm$

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

export enum Gender {
  MALE = 'Male',
  FEMALE = 'Female',
  Other = 'Other',
}

export interface Name {
  firstname: string;
  lastname: string;
}

export interface Address {
  streetaddress: string;
  city: string;
  state: string;
  zip: string;
  country: string;
}

export interface Phone {
  phone: string;
  countrycode: string;
}

export interface PersonalDetails {
  name: Name;
  gender: Gender;
  address: Address;
  phone: Phone;
}

export interface MainForm {
  // this is one example out of what you posted
  personalDetails: PersonalDetails;

  // you'll probably want to add 'parent' and 'responsibilities' here too
  // which I'm not going to do because 'personalDetails' covers it all :)
}

Затем мы можем создать компонент, который расширяет NgxSubFormComponent.
Позвольте назвать это personal-details-form.component.

@Component({
  selector: 'app-personal-details-form',
  templateUrl: './personal-details-form.component.html',
  styleUrls: ['./personal-details-form.component.css'],
  providers: subformComponentProviders(PersonalDetailsFormComponent)
})
export class PersonalDetailsFormComponent extends NgxSubFormComponent<PersonalDetails> {
  protected getFormControls(): Controls<PersonalDetails> {
    return {
      name: new FormControl(null, { validators: [Validators.required] }),
      gender: new FormControl(null, { validators: [Validators.required] }),
      address: new FormControl(null, { validators: [Validators.required] }),
      phone: new FormControl(null, { validators: [Validators.required] }),
    };
  }
}

Несколько вещей, чтобы заметить здесь:

  • NgxSubFormComponent<PersonalDetails> собирается обеспечить нам безопасность типов
  • Мы должны реализовать методы getFormControls которые ожидают словарь ключей верхнего уровня, соответствующих абстрактному getFormControls управления (здесь name, gender, address, phone)
  • Мы сохраняем полный контроль над опциями создания formControl (валидаторы, асинхронные валидаторы и т.д.)
  • providers: subformComponentProviders(PersonalDetailsFormComponent) - это небольшая служебная функция для создания поставщиков, необходимых для использования ControlValueAccessor (см. Angular doc), вам просто нужно передать в качестве аргумента текущий компонент

Теперь для каждой записи name, gender, address, phone который является объектом, мы создаем для него субформу (поэтому в данном случае все, кроме gender).

Вот пример с телефоном:

@Component({
  selector: 'app-phone-form',
  templateUrl: './phone-form.component.html',
  styleUrls: ['./phone-form.component.css'],
  providers: subformComponentProviders(PhoneFormComponent)
})
export class PhoneFormComponent extends NgxSubFormComponent<Phone> {
  protected getFormControls(): Controls<Phone> {
    return {
      phone: new FormControl(null, { validators: [Validators.required] }),
      countrycode: new FormControl(null, { validators: [Validators.required] }),
    };
  }
}

Теперь давайте напишем шаблон для него:

<div [formGroup]="formGroup">
  <input type="text" placeholder="Phone" [formControlName]="formControlNames.phone">
  <input type="text" placeholder="Country code" [formControlName]="formControlNames.countrycode">
</div>

Заметить, что:

  • Мы определяем <div [formGroup]="formGroup">, здесь formGroup предоставляется NgxSubFormComponent вам не нужно создавать его самостоятельно
  • [formControlName]="formControlNames.phone" мы используем привязку свойства, чтобы иметь динамическое formControlName а затем используем formControlNames. Этот механизм безопасности типов также предлагается NgxSubFormComponent и если в какой-то момент ваш интерфейс изменится (мы все знаем о рефакторах...), не только ваш TS выдаст ошибку из-за отсутствующих свойств в форме, но также и HTML (когда вы компилируете с AOT )!

Следующий шаг: давайте PersonalDetailsFormComponent шаблон PersonalDetailsFormComponent но сначала просто добавим эту строку в TS: public Gender: typeof Gender = Gender; так что мы можем безопасно получить доступ к перечислению с точки зрения

<div [formGroup]="formGroup">
    <app-name-form [formControlName]="formControlNames.name"></app-name-form>

    <select [formControlName]="formControlNames.gender">
    <option *ngFor="let gender of Gender | keyvalue" [value]="gender.value">{{ gender.value }}</option>
  </select>

  <app-address-form [formControlName]="formControlNames.address"></app-address-form>

  <app-phone-form [formControlName]="formControlNames.phone"></app-phone-form>
</div>

Обратите внимание, как мы делегируем ответственность подкомпоненту? <app-name-form [formControlName]="formControlNames.name"></app-name-form> что ключевой момент здесь!

Заключительный шаг: построение верхнего компонента формы

Хорошие новости, мы также можем использовать NgxSubFormComponent для обеспечения безопасности типов!

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent extends NgxSubFormComponent<MainForm> {
  protected getFormControls(): Controls<MainForm> {
    return {
      personalDetails: new FormControl(null, { validators: [Validators.required] }),
    };
  }
}

И шаблон:

<form [formGroup]="formGroup">
  <app-personal-details-form [formControlName]="formControlNames.personalDetails"></app-personal-details-form>
</form>

<!-- let see how the form values looks like! -->
<h1>Values:</h1>
<pre>{{ formGroupValues | json }}</pre>

<!-- let see if there any error (works with nested ones!) -->
<h1>Errors:</h1>
<pre>{{ formGroupErrors | json }}</pre>

enter image description here

Вывод из всего этого: - Введите безопасные формы - Многоразовые! Нужно повторно использовать адрес один для parents? Конечно, не беспокойтесь - Хорошие утилиты для создания вложенных форм, имен элементов управления доступом, значений форм, ошибок форм (+nested!) - Вы заметили какую-либо сложную логику вообще? Никаких наблюдаемых, никаких сервисов для внедрения... Просто определяя интерфейсы, расширяя класс, передавая объект с помощью элементов управления формы и создавая представление. Это

Кстати, вот живая демонстрация всего, о чем я говорил:
https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling

Кроме того, это не было необходимо в этом случае, но для форм, немного более сложных, например, когда вам нужно обработать полиморфный объект, такой как type Animal = Cat | Dog type Animal = Cat | Dog у нас есть еще один класс для этого, который является NgxSubFormRemapComponent но вы можете прочитать README, если вам нужно больше информации.

Надеюсь, это поможет вам масштабировать ваши формы!

Редактировать:

Если вы хотите пойти дальше, я только что опубликовал пост в блоге, чтобы объяснить много вещей о формах и ngx-sub-form здесь https://dev.to/maxime1992/building-scalable-robust-and-type- сейф-форм-с-угловому-3nf9

Ответ 3

Я сделал подобное приложение. Проблема в том, что вы одновременно создаете все свои входы, что вряд ли масштабируется.

В моем случае я создал FormManagerService, который управляет массивом FormGroup. На каждом шаге есть FormGroup, который инициализируется один раз в выполнении на ngOnInit шагового компонента, отправляя его конфигурацию FormGroup в FormManagerService. Что-то вроде этого:

stepsForm: Array<FormGroup> = [];
getFormGroup(id:number, config: Object): FormGroup {
    let formGroup: FormGroup;
    if(this.stepsForm[id]){
        formGroup = this.stepsForm[id];
    } else {
        formGroup = this.createForm(config); // call function to create FormGroup
        this.stepsForm[id] = formGroup;
    }
    return formGroup;
}

Вам понадобится идентификатор, чтобы узнать, какая FormGroup соответствует этапу. Но после этого вы сможете разделить конфигурацию форм на каждом шаге (так что небольшие файлы конфигурации, которые легче обслуживать, чем огромный файл). Это минимизирует начальное время загрузки, поскольку FormGroups создаются только при необходимости.

Наконец, перед отправкой вам просто нужно сопоставить массив FormGroup и проверить, все ли они действительны. Просто убедитесь, что все шаги были посещены (иначе некоторые FormGroup не будут созданы).

Это может быть не лучшее решение, но это было хорошо в моем проекте, так как я заставляю пользователя следовать моим шагам. Дайте мне свои отзывы.:)

Ответ 4

Этот ответ приходит с оговоркой, что я взломщик, который в основном ничего не знает. Пожалуйста, не стесняйтесь разорвать это на части, если это просто неправильно. Для меня, по крайней мере, я не понимаю ответа Ovangle, достаточного для реализации, и мне нужно знать, как сделать FormArray, чтобы использовать библиотеку Maxime1992, которая выглядит потрясающе.

Обойдя круги, не найдя много примеров форм, выходящих за пределы одной формы, одного компонента и найдя этот старый вопрос, который задавал 90% того, что я хотел знать (подчиняется различным маршрутам), я пришел к следующему шаблону, который я Поделюсь на случай, если это пригодится кому-то еще:

Шаблон

  • Отдельные FormGroup создают Сервис, который предоставляет FormGroup плюс методы Create & Delete.
  • Контейнер использует эту службу и передает их дочерним компонентам (Form & Table)
  • Форма, которая обновляет выбранную строку таблицы.
  • Таблица представления для отображения (чтения) данных формы. Строки можно щелкнуть излучающей строкой для редактирования формы

Основная форма импортирует физическим лицам услуги субформ.

Грубый стекблиц - https://stackblitz.com/edit/angular-uzmdmu-merge-formgroups

Schematic of form service

Дайте мне знать, если это можно улучшить

РЕДАКТИРОВАТЬ: по запросу

что вы ожидаете именно

Я ищу шаблон/пример, в котором отдельные формы, такие как personalDetailsForm, должностные personalDetailsForm responsibilitiesForm могут разрабатываться отдельно в виде отдельной страницы (в моем случае это показано на iPad, поэтому у меня не может быть только одной большой формы) с их собственными маршрутами, такими как "/personal"/обязанности. Затем, аналогично OP, я хочу собрать формы в сводную страницу и проверить правильность. Повторное использование любых презентационных частей идеально.

отредактируйте свой ответ, чтобы упомянуть случай с FormArrays

Так, например, как бы вы реализовали несколько адресов в personalDetailsForm? Особенно, если формы находятся на их собственных маршрутах или далее, если адреса были в их собственном под-маршруте.

Ответ 5

Действительно ли необходимо сохранить элементы управления формы в сервисе? Почему бы просто не оставить службу в качестве хранителя данных и иметь элементы управления формой в компонентах? Вы можете использовать защиту CanDeactivate, чтобы пользователь не мог перейти от компонента с недопустимыми данными.

https://angular.io/docs/ts/latest/api/router/index/CanDeactivate-interface.html