首页 文章

在Angular中包装FormControl(2)

提问于
浏览
7

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

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

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

例:

Outer component, containing a form and using custom component

<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>

Custom component template

基本上我的自定义表单组件只包含一个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 更改时,该值应该用作表单值 .

我尝试过的事情

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

简单事件绑定

绑定到 input 上的 keyupblur 事件,然后通知父更改(即调用Angular传入 registerOnChange 的函数作为实现 ControlValueAccessor 的一部分) .

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

它也不考虑验证(例如,如果它是“必需的”,当值为n时; t设置表单控件将正确无效,但Angular Material组件将不会如此显示) .

嵌套表格

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

这种作品,但感觉凌乱,应该有一个更好的方法 .

这也意味着应用于我的自定义表单控件(由容器组件)的任何验证都会被忽略,因为我创建了一个缺少原始验证的新“内部表单” .

根本不要使用ControlValueAccessor,而只是传入表单

正如 Headers 所说......我试图不以“正确”的方式做到这一点,而是添加了一个绑定到父表单 . 然后,我在自定义组件中创建一个表单控件作为该父表单的一部分 .

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

摘要

处理这个问题的正确方法是什么?感觉我只是在绊倒不同的反模式,但我在文档中找不到任何暗示甚至支持它的东西 .

3 回答

  • 3

    Edit:

    我've added a helper for doing just this an angular utilities library I'已经开始了:s-ng-utils . 使用它你可以扩展 WrappedFormControlSuperclass 并写:

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

    查看更多文档here .


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

    @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 更改为材料下拉列表的值访问者 - 我自己并不熟悉Material .

  • 2

    我实际上已经把这个问题包围了一段时间,我找到了一个非常相似(或相同)的好解决方案,因为Eric _840029使用了@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);
      }
    }
    
  • 0

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

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

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

相关问题