web-dev-qa-db-ja.com

Angular(2+)でFormControlをラップする

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オートコンプリート付きのマテリアルドロップダウンをラップするだけです。カスタムコンポーネントを作成せずにこれを行うことができますが、すべてのコードと同じようにこれを行うのは理にかなっているようですフィルタリングなどを処理するために、コンテナクラス(これの実装を気にする必要はありません)ではなく、そのコンポーネントクラス内に存在できます。

<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の変更時には、その値をフォームの値として使用する必要があります。

私が試したもの

私はこれを行ういくつかの方法を試しましたが、すべて独自の落とし穴があります:

単純なイベントバインディング

keyupblurおよびinputイベントにバインドし、変更を親に通知します(つまり、Angular pass registerOnChangeの実装の一部としてControlValueAccessorに)。

この種の作品ですが、ドロップダウンから値を選択すると、変更イベントが発生せず、一貫性のない状態になります。

また、検証も考慮されません(たとえば、値が設定されていない場合に「必須」の場合、フォームコントロールは正しく無効になりますが、Angular Materialコンポーネントは次のように表示されません。そのような)。

ネストされたフォーム

これは少し近いです。単一のコントロールを持つカスタムコンポーネントクラス内に新しいフォームを作成しました。コンポーネントテンプレートで、そのフォームコントロールをAngular Materialコンポーネントに渡します。クラスでは、そのvalueChangesをサブスクライブし、変更を親に反映します(registerOnChangeに渡される関数を介して)。

この種の作品は散らかっていますが、もっと良い方法があるはずだと感じています。

また、元の検証を欠いた新しい「内部フォーム」を作成したため、(コンテナーコンポーネントによって)カスタムフォームコントロールに適用された検証は無視されます。

ControlValueAccessorはまったく使用せず、代わりに次の形式で渡してください

タイトルが言うように...私はこれを「適切な」方法で行わないようにして、代わりに親フォームにバインディングを追加しました。次に、その親フォームの一部としてカスタムコンポーネント内にフォームコントロールを作成します。

これは値の更新とある程度の検証を処理するために機能します(ただし、親フォームではなくコンポーネントの一部として作成する必要があります)が、これは間違っていると感じているだけです。

概要

これを処理する適切な方法は何ですか?さまざまなアンチパターンを見つけたような気がしますが、ドキュメントでこれがサポートされていることを示唆するものは見つかりません。

19
Tom Seldon

編集:

これを実行するためのヘルパーを追加しましたangular始めたユーティリティライブラリ: s-ng-utils 。これを使用して、WrappedFormControlSuperclass そして書く:

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

その他のドキュメント こちら をご覧ください。


1つの解決策は、内部フォームコンポーネント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は、手動でangularをトリガーして、通常のDefaultValueAccessorを入力にアタッチします。これは、_<input ngModel>_を使用すると自動的に行われます、ただしここではngModelを使用せず、値アクセサーのみを使用します。上記のDefaultValueAccessorを、マテリアルドロップダウンの値アクセサーの値に変更する必要があります。自分で素材を使って。

7
Eric Simonton

私は実際にしばらく頭を抱えていて、Ericと非常によく似た(または同じ)良い解決策を見つけました。彼が説明するのを忘れていたのは、ビューが実際にロードされるまで@ViewChild valueAccessorを使用できないことです( @ ViewChild docs を参照)。

これが解決策です:(コアをラップする私の例を示しますangular selectディレクティブを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);
  }
}
3
Max101