创建一个可重用的FormGroup

时间:2017-08-15 16:01:42

标签: angular angular-forms

我知道将自定义控件创建为组件,但我无法弄清楚如何创建自定义

我们可以通过implementing ControlValueAccessor并使用<my-cmp formControlName="foo"></my-cmp>之类的自定义组件执行此操作,我们如何才能为a group实现此效果?

<my-cmp formGroupName="aGroup"></my-cmp>

两个非常常见的用例是:(a)将长格式分为几个步骤,每个步骤分别在一个单独的组件中;(b)封装一组字段,这些字段以多种形式出现,例如地址(国家组,州,城市,地址,建筑物编号)或出生日期(年,月,日)。

示例用法(不是实际工作代码)

Parent使用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">

3 个答案:

答案 0 :(得分:26)

rusev's 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) {}
}

您可以看到live demo on StackBlitz

这是对rusev在几个方面的答案的改进:

  • 没有自定义groupName输入;相反,我们使用Angular提供的formGroupName
  • 不需要@SkipSelf装饰器,因为我们注入父控件,但我们需要的是
  • 没有尴尬的group.control.get(groupName)这是为了抓住自己而去父母。

答案 1 :(得分: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: '',
      })
    });
  }
}

答案 2 :(得分:0)

我找到了一种动态的方法,可以使用反应表单

关于这个主题的问题和帖子的研究,我找到了构建一个绑定父表单的指令和动态子组件的材料。好的一点是,您不需要在父组件中定义所有表单,但每个FormGroup都是独立的,并且与指令有效。

我将其命名为BindFormDirective,它会收到parentchild个实施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>

如果完整插图需要剩下的代码,我会花一些额外的时间来更新我的答案。 享受!