以Angular(2 +)

时间:2017-11-19 13:30:16

标签: forms angular angular-material

我试图在Angular(v5)中创建自定义表单控件。自定义控件本质上是Angular Material组件的包装器,但还有一些额外的东西。

我已经阅读了有关实施ControlValueAccessor的各种教程,但我无法找到任何可以编写组件以包装现有组件的内容。

理想情况下,我想要一个显示Angular Material组件的自定义组件(带有一些额外的绑定和内容),但是能够从父表单传递验证(例如required)并拥有Angular Material组件处理它。

示例:

外部组件,包含表单并使用自定义组件

<form [formGroup]="myForm">
    <div formArrayName="things">
        <div *ngFor="let thing of things; let i = index;">
            <app-my-custom-control [formControlName]="i"></app-my-custom-control>
        </div>
    </div>
</form>

自定义组件模板

基本上我的自定义表单组件只包含一个带有自动完成功能的Angular Material下拉列表。我可以在不创建自定义组件的情况下做到这一点,但是这样做似乎是有意义的,因为处理过滤等的所有代码都可以存在于该组件类中,而不是在容器类中(它不是在容器类中)。我需要关心这个的实施。)

<mat-form-field>
  <input matInput placeholder="Thing" aria-label="Thing" [matAutocomplete]="thingInput">
  <mat-autocomplete #thingInput="matAutocomplete">
    <mat-option *ngFor="let option of filteredOptions | async" [value]="option">
      {{ option }}
    </mat-option>
  </mat-autocomplete>
</mat-form-field>

因此,在input更改时,该值应该用作表单值。

我尝试的事情

我尝试了几种方法,但都有自己的陷阱:

简单事件绑定

绑定到keyup上的blurinput个事件,然后通知父级更改(即调用Angular传入registerOnChange的函数作为实施ControlValueAccessor)。

这种方式有效,但是从下拉列表中选择一个值时,似乎变更事件不会触发,并且最终会处于不一致的状态。

它也没有考虑验证(例如,如果它&#34;需要&#34;,当值不是; t设置表单控件将正确无效,但Angular Material组件不会表现出来。

嵌套表格

这有点接近了。我在自定义组件类中创建了一个新表单,该表单具有单个控件。在组件模板中,我将该表单控件传递给Angular Material组件。在类中,我订阅了valueChanges,然后将更改传播回父级(通过传递给registerOnChange的函数)。

这种作品,但感觉很乱,就像应该有更好的方法。

这也意味着我的自定义表单控件(由容器组件)应用的任何验证都会被忽略,因为我已经创建了一个新的内部表单&#34;缺乏原始验证。

根本不使用ControlValueAccessor,而只是传递表格

正如标题所说......我试着不做这个&#34;正确的&#34;方式,而是添加到父窗体的绑定。然后,我在自定义组件中创建一个表单控件,作为该父表单的一部分。

这适用于处理值更新和扩展验证(但必须创建为组件的一部分,而不是父表单),但这只是错误。

摘要

处理此问题的正确方法是什么?感觉我只是绊倒了不同的反模式,但我无法在文档中找到任何暗示甚至支持它的内容。

4 个答案:

答案 0 :(得分:7)

修改

我已经添加了一个帮助程序来执行这个我已启动的角度实用程序库:s-ng-utils。使用它你可以扩展WrappedFormControlSuperclass并写:

@Component({
  selector: 'my-wrapper',
  template: '<input [formControl]="formControl">',
  providers: [provideValueAccessor(MyWrapper)],
})
export class MyWrapper extends WrappedFormControlSuperclass<string> {
  // ...
}

查看更多文档here

一种解决方案是获取与内部表单组件@ViewChild()相对应的ControlValueAccessor,并在您自己的组件中委派给它。例如:

@Component({
  selector: 'my-wrapper',
  template: '<input ngDefaultControl>',
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => NumberInputComponent),
      multi: true,
    },
  ],
})
export class MyWrapper implements ControlValueAccessor {
  @ViewChild(DefaultValueAccessor) private valueAccessor: DefaultValueAccessor;

  writeValue(obj: any) {
    this.valueAccessor.writeValue(obj);
  }

  registerOnChange(fn: any) {
    this.valueAccessor.registerOnChange(fn);
  }

  registerOnTouched(fn: any) {
    this.valueAccessor.registerOnTouched(fn);
  }

  setDisabledState(isDisabled: boolean) {
    this.valueAccessor.setDisabledState(isDisabled);
  }
}

上面模板中的ngDefaultControl是手动触发角度以将其正常DefaultValueAccessor附加到输入。如果您使用<input ngModel>,则会自动执行此操作,但我们不希望此处为ngModel,只需要值访问器。您需要将上面的DefaultValueAccessor更改为材料下拉菜单的值访问者 - 我自己并不熟悉材料。

答案 1 :(得分:2)

我实际上已经绕过这个问题一段时间了,我找到了一个与埃里克非常相似(或相同)的好解决方案。 他忘记考虑的是,在实际加载视图之前,您无法使用@ViewChild valueAccessor(参见@ViewChild docs

这是解决方案:(我给你的例子是用NgModel包装核心角度选择指令,因为你使用的是自定义formControl,你需要定位那个formControl的valueAccessor类)

@Component({
  selector: 'my-country-select',
  templateUrl: './country-select.component.html',
  styleUrls: ['./country-select.component.scss'],
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting:  CountrySelectComponent,
    multi: true
  }]
})
export class CountrySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {

  @ViewChild(SelectControlValueAccessor) private valueAccessor: SelectControlValueAccessor;

  private country: number;
  private formControlChanged: any;
  private formControlTouched: any;

  public ngAfterViewInit(): void {
    this.valueAccessor.registerOnChange(this.formControlChanged);
    this.valueAccessor.registerOnTouched(this.formControlTouched);
  }

  public registerOnChange(fn: any): void {
    this.formControlChanged = fn;
  }

  public registerOnTouched(fn: any): void {
    this.formControlTouched = fn;
  }

  public writeValue(newCountryId: number): void {
    this.country = newCountryId;
  }

  public setDisabledState(isDisabled: boolean): void {
    this.valueAccessor.setDisabledState(isDisabled);
  }
}

答案 2 :(得分:1)

我参加聚会有点晚了,这是我包装一个可以接受formControlNameformControlngModel

的组件的过程
@Component({
  selector: 'app-input',
  template: '<input [formControl]="control">',
  styleUrls: ['./app-input.component.scss']
})
export class AppInputComponent implements OnInit, ControlValueAccessor {
  constructor(@Optional() @Self() public ngControl: NgControl) {
    if (this.ngControl != null) {
      // Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
      this.ngControl.valueAccessor = this;
    }
  }

  control: FormControl;

  // These are just to make Angular happy. Not needed since the control is passed to the child input
  writeValue(obj: any): void { }
  registerOnChange(fn: (_: any) => void): void { }
  registerOnTouched(fn: any): void { }

  ngOnInit() {
    if (this.ngControl instanceof FormControlName) {
      const formGroupDirective = this.ngControl.formDirective as FormGroupDirective;
      if (formGroupDirective) {
        this.control = formGroupDirective.form.controls[this.ngControl.name] as FormControl;
      }
    } else if (this.ngControl instanceof FormControlDirective) {
      this.control = this.ngControl.control;
    } else if (this.ngControl instanceof NgModel) {
      this.control = this.ngControl.control;
      this.control.valueChanges.subscribe(x => this.ngControl.viewToModelUpdate(this.control.value));
    } else if (!this.ngControl) {
      this.control = new FormControl();
    }
  }
}

答案 3 :(得分:0)

NgForm提供了一种简单的方法来管理表单,而无需在HTML表单中注入任何数据。输入数据必须在组件级别注入,而不是在经典的html标记中注入。

<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)>...</form>

其他方法是创建一个表单组件,其中所有数据模型都使用ngModel绑定;)