Создание многоразовой FormGroup
Мне известно о создании пользовательских элементов управления в качестве компонентов, но я не могу понять, как создавать пользовательские группы.
То же самое мы можем сделать это, реализовав ControlValueAccessor
и используя пользовательский компонент, такой как <my-cmp formControlName="foo"></my-cmp>
, как мы можем достичь этого эффекта для группы?
<my-cmp formGroupName="aGroup"></my-cmp>
Два очень распространенных варианта использования: (а) разделение длинной формы на этапы, каждый шаг в отдельном компоненте, и (б) инкапсуляция группы полей, которые появляются в нескольких формах, таких как адрес (группа страны, штат, город), адрес, номер здания) или дата рождения (год, месяц, дата).
Пример использования (не фактический рабочий код)
Родитель имеет следующую форму, FormBuilder
с помощью FormBuilder
:
// parent model
form = this.fb.group({
username: '',
fullName: '',
password: '',
address: this.fb.group({
country: '',
state: '',
city: '',
street: '',
building: '',
})
})
Родительский шаблон (недоступный и несемантический для краткости):
<!-- parent template -->
<form [groupName]="form">
<input formControlName="username">
<input formControlName="fullName">
<input formControlName="password">
<address-form-group formGroup="address"></address-form-group>
</form>
Теперь этот AddressFormGroupComponent
знает, как обрабатывать группу, в которой есть эти конкретные элементы управления.
<!-- child template -->
<input formControlName="country">
<input formControlName="state">
<input formControlName="city">
<input formControlName="street">
<input formControlName="building">
Ответы
Ответ 1
Часть, которую я отсутствовала, упоминалась в rusev answer, и это впрыскивание ControlContainer
.
Оказывается, если вы поместите formGroupName
на компонент, и если этот компонент введет ControlContainer
, вы получите ссылку на контейнер, который содержит эту форму. Это легко отсюда.
Создаем компонент подформы.
@Component({
selector: 'sub-form',
template: `
<ng-container [formGroup]="controlContainer.control">
<input type=text formControlName=foo>
<input type=text formControlName=bar>
</ng-container>
`,
})
export class SubFormComponent {
constructor(public controlContainer: ControlContainer) {
}
}
Обратите внимание, как нам нужна оболочка для входов. Мы не хотим формы, потому что это уже будет внутри формы. Поэтому мы используем ng-container
. Это будет отделено от окончательной DOM, поэтому нет лишнего элемента.
Теперь мы можем просто использовать этот компонент.
@Component({
selector: 'my-app',
template: `
<form [formGroup]=form>
<sub-form formGroupName=group></sub-form>
<input type=text formControlName=baz>
</form>
`,
})
export class AppComponent {
form = this.fb.group({
group: this.fb.group({
foo: 'foo',
bar: 'bar',
}),
baz: 'baz',
})
constructor(private fb: FormBuilder) {}
}
Вы можете увидеть живое демо на StackBlitz.
Это улучшение по сравнению с русевским ответом в нескольких аспектах:
- нет пользовательских
groupName
ввода; вместо этого мы используем formGroupName
, предоставляемый Angular
- Не нужно для декоратора
@SkipSelf
, так как мы не вводим родительский элемент управления, а тот, который нам нужен
- no awkward
group.control.get(groupName)
, который идет к родительскому, чтобы захватить себя.
Ответ 2
Angular формы не имеют понятия для имени группы, как для имени управления формой. Однако вы можете легко обойти это, обернув дочерний шаблон в группу форм.
Вот пример, похожий на разметку, которую вы разместили - https://plnkr.co/edit/2AZ3Cq9oWYzXeubij91I?p=preview
@Component({
selector: 'address-form-group',
template: `
<!-- child template -->
<ng-container [formGroup]="group.control.get(groupName)">
<input formControlName="country">
<input formControlName="state">
<input formControlName="city">
<input formControlName="street">
<input formControlName="building">
</ng-container>
`
})
export class AddressFormGroupComponent {
@Input() public groupName: string;
constructor(@SkipSelf() public group: ControlContainer) { }
}
@Component({
selector: 'my-app',
template: `
<!-- parent template -->
<div [formGroup]="form">
<input formControlName="username">
<input formControlName="fullName">
<input formControlName="password">
<address-form-group groupName="address"></address-form-group>
</div>
{{form?.value | json}}
`
})
export class AppComponent {
public form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
username: '',
fullName: '',
password: '',
address: this.fb.group({
country: '',
state: '',
city: '',
street: '',
building: '',
})
});
}
}
Ответ 3
Я нашел динамический способ сделать это с помощью Реактивных форм.
Изучая проблемы и сообщения по этой теме, я нашел материал для создания директивы, которая ограничивает родительскую форму с динамическими дочерними компонентами. Оптимальным является то, что вам не нужно определять всю форму в родительском компоненте, но каждая FormGroup является независимой и ограниченной директивой.
Я назвал его BindFormDirective
, и он получает компоненты parent
и child
, которые реализуют интерфейс BindForm
(у них есть открытый элемент form: FormGroup
для управления), и они предоставляют BINDFORM_TOKEN
.
Директива получает значение как имя дочерней группы, а его код выглядит следующим образом:
import { ChangeDetectorRef, Directive, Inject, InjectionToken, Input, OnDestroy, OnInit, Self, SkipSelf } from '@angular/core';
import { FormGroup } from '@angular/forms';
export interface BindForm {
form: FormGroup;
}
export const BINDFORM_TOKEN = new InjectionToken<BindForm>('BindFormToken');
@Directive({
selector: '[bindForm]'
})
export class BindFormDirective implements OnInit, OnDestroy {
private controlName = null;
@Input()
set binForm(value) {
if (this.controlName) {
throw new Error('Cannot change the bindName on runtime!');
}
this.controlName = value;
}
constructor(
private cdr: ChangeDetectorRef,
@Inject(BINDFORM_TOKEN) @SkipSelf() private parent: BindForm,
@Inject(BINDFORM_TOKEN) @Self() private child: BindForm
) {}
ngOnInit() {
if (!this.controlName) {
throw new Error('BindForm directive requires a value to be used as the subgroup name!');
}
if (this.parent.form.get(this.controlName)) {
throw new Error(`That name (${this.controlName}) already exists on the parent form!`);
}
// add a child control under the unique name
this.parent.form.addControl(this.controlName, this.child.form);
this.cdr.detectChanges();
}
ngOnDestroy() {
// remove the component from the parent
this.parent.form.removeControl(this.controlName);
}
}
С другой стороны, задействованные компоненты должны предоставлять BINDFORM_TOKEN
в своем определении @Component
, чтобы иметь возможность инъекции по директиве и реализовать интерфейс BindForm
, например:
@Component({
...
providers: [
{
provide: BINDFORM_TOKEN,
useExisting: forwardRef(() => MyFormComponent)
}
]
})
export class MyFormComponent implements BindForm, OnInit {
form: FormGroup;
...
Итак, вы произвольно реализуете свои различные компоненты формы и связываете FormGroups друг с другом, вы просто используете директиву в своем компоненте родительской формы:
<form [formGroup]="form" ...>
<my-step1 bindForm="step1"></my-step1>
</form>
Если оставшийся код необходим для полной иллюстрации, я бы выделил некоторое дополнительное время для обновления моего ответа позже.
Наслаждайтесь!
Ответ 4
@Матео, ваше решение великолепно, есть ли возможность динамически добавлять пользовательские компоненты управления?