Создание многоразовой 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

@Матео, ваше решение великолепно, есть ли возможность динамически добавлять пользовательские компоненты управления?