Angular 2 - обработка больших заявок
В компании, в которой я работаю, мы разрабатываем крупномасштабное приложение с несколькими формами, которое пользователь должен заполнить, чтобы зарегистрироваться в нашей программе. Когда на все вопросы дан ответ, пользователь попадает в раздел, в котором суммируются все его ответы, выделяются неверные ответы и дается возможность пользователю вернуться к любому из предыдущих шагов формы и пересмотреть свои ответы. Эта логика будет повторяться для ряда разделов верхнего уровня, каждый из которых имеет несколько шагов / страниц и страницу сводки.
Для этого мы создали компонент для каждого отдельного шага формы (это такие категории, как "Личные данные" или "Квалификации" и т. Д.) Вместе с соответствующими маршрутами и компонентом для страницы "Сводка".
Чтобы сохранить его максимально сухим, мы начали создавать "главный" сервис, который содержит информацию для всех различных шагов формы (значения, достоверность и т. Д.).
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, позволяет нам сохранить нашу иерархию валидации без изменений, а также является простым.
Есть ли лучший способ достичь того, что мы пытаемся сделать?
3 ответа
Я прокомментировал в другом месте о @ngrx/store
и, хотя я все еще рекомендую это, я полагаю, что немного неправильно понял вашу проблему.
Во всяком случае, ваш FormsControlService
в основном глобальный конст. Серьезно, замените export class FormControlService ...
с
export const formControlsDefinitions = {
// ...
};
и какая разница? Вместо того, чтобы получить услугу, вы просто импортируете объект. И так как мы теперь думаем об этом как о типизированном const global, мы можем определить интерфейсы, которые мы используем...
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. }
}
затем создайте где-нибудь модуль манифеста (который внедряется в модуль приложения "ядро"), создав свой 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 {}
Теперь у нас есть решение, которое:
- более локальные элементы управления формой объявляются вместе с моделями
- более масштабируемый, просто нужно добавить элемент управления формы и затем добавить его в модуль манифеста; а также
- менее монолитный, нет "бог" конфигурации классов.
Ваш подход и подход Ovangle кажутся довольно хорошими, но, несмотря на то, что этот SO вопрос решен, я хочу поделиться своим решением, потому что это действительно другой подход, который, я думаю, вам может понравиться или может быть полезен для кого-то еще.
какие решения существуют для широкой формы приложения, где Компоненты заботятся о различных подчастях к глобальной форме.
Мы столкнулись с точно такой же проблемой, и после нескольких месяцев борьбы с огромными, вложенными и иногда полиморфными формами мы нашли решение, которое нас порадует, простое в использовании и которое дает нам "суперсилы" (типа безопасность как в TS, так и в HTML), доступ к вложенным ошибкам и другие.
Мы решили извлечь это в отдельную библиотеку и открыть ее.
Исходный код доступен здесь: https://github.com/cloudnc/ngx-sub-form
И пакет npm может быть установлен так npm i ngx-sub-form
За кулисами наша библиотека использует ControlValueAccessor
и это позволяет нам использовать его в шаблонных формах И реактивных формах (вы получите лучшее из этого, используя реактивные формы).
Так о чем это все?
Ну, пример стоит 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
методы, которые ожидают словарь ключей верхнего уровня, соответствующих абстрактному элементу управления (здесь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
шаблон, но сначала просто добавьте эту строку в 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's any error (works with nested ones!) -->
<h1>Errors:</h1>
<pre>{{ formGroupErrors | json }}</pre>
Вывод из всего этого: - Введите безопасные формы - Многоразовые! Необходимо повторно использовать адрес один для parents
? Конечно, не беспокойтесь - Хорошие утилиты для создания вложенных форм, имен элементов управления доступом, значений форм, ошибок форм (+ вложенные!) - Вы заметили какую-либо сложную логику вообще? Никаких наблюдаемых, никаких сервисов для внедрения... Просто определяя интерфейсы, расширяя класс, передавая объект с помощью элементов управления формы и создавая представление. это оно
Кстати, вот живая демонстрация всего, о чем я говорил:
https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling
Кроме того, это не было необходимо в этом случае, но для форм, немного более сложных, например, когда вам нужно обработать полиморфный объект, такой как type Animal = Cat | Dog
у нас есть еще один класс для того, что NgxSubFormRemapComponent
но вы можете прочитать README, если вам нужно больше информации.
Надеюсь, это поможет вам масштабировать ваши формы!
Я сделал подобное заявление. Проблема в том, что вы создаете все свои входы одновременно, что вряд ли масштабируется.
В моем случае я сделал 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 соответствует шагу. Но после этого вы сможете разделять конфигурацию Forms на каждом шаге (такие маленькие файлы конфигурации, которые легче обслуживать, чем огромный файл). Это минимизирует начальное время загрузки, поскольку FormGroups создаются только при необходимости.
Наконец, перед отправкой вам просто нужно сопоставить массив FormGroup и проверить, все ли они действительны. Просто убедитесь, что все шаги были посещены (в противном случае не будет создана группа FormGroup).
Возможно, это не лучшее решение, но оно подходило для моего проекта, так как я заставляю пользователя следовать моим шагам. Дайте мне свой отзыв.:)
Этот ответ приходит с оговоркой, что я взломщик, который в основном ничего не знает. Пожалуйста, не стесняйтесь разорвать это на части, если это просто неправильно. Для меня, по крайней мере, я не понимаю достаточно ответа Ovangle для реализации, и мне нужно знать, как сделать FormArray, чтобы использовать библиотеку Maxime1992, которая выглядит потрясающе.
Обойдя круги, не найдя много примеров форм, выходящих за пределы одной формы, одного компонента и найдя этот старый вопрос, который задавал 90% того, что я хотел знать (подчиняется различным маршрутам), я пришел к следующему шаблону, который я делюсь на случай, если это пригодится кому-то еще:
Шаблон
- Отдельные формы создают Сервис, который обеспечивает
FormGroup
плюс методы создания и удаления. - Контейнер использует эту службу и передает их дочерним компонентам (Form & Table)
- Форма, которая обновляет выбранную строку таблицы.
- Таблица представления для отображения (чтения) данных формы. Строки можно щелкнуть излучающей строкой для редактирования формы
Основная форма импортирует физическим лицам услуги субформ.
Грубый стекблиц - https://stackblitz.com/edit/angular-uzmdmu-merge-formgroups
Дайте мне знать, если это можно улучшить
Действительно ли необходимо сохранять элементы управления формой в сервисе? Почему бы просто не оставить службу в качестве хранителя данных и иметь элементы управления формой в компонентах? Вы могли бы использовать CanDeactivate
Защита для предотвращения перехода пользователя от компонента с недопустимыми данными.
https://angular.io/docs/ts/latest/api/router/index/CanDeactivate-interface.html