[Angular] カスタムコンポーネント(Custom Component)で ngModel を使う


カスタムコンポーネント(独自コンポーネント)で ngModel を使いたいというケースって結構あると思います。
ngModel は、 [(ngModel)] と書く通り、双方向バインドで あり、普通の @Input, @Output ではありません。
そのため、ちょっと特殊な書き方が必要になります。

今回作るコンポーネント

label+inputという簡単なコンポーネント

htmlは下記のような感じを想定

<div>
  <label> {{ label }} </label>
  <input [(ngModel)]="value">
</div>

ngModel を独自コンポーネントで実装する手順

最初に、手順をまとめると、下記のようになります。

  1. @Componentprovider を追加
  2. ControlValueAccessor を implement
  3. valueの getter , setter を作成

@Componentprovider を追加


@Component({
  selector: 'app-test-ng-model',
  templateUrl: './test-ng-model.component.html',
  styleUrls: ['./test-ng-model.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: forwardRef(() => TestNgModelComponent),
    },
  ],
})

上記 providers を追加する。 ngModel を利用するには、 ControlValueAccessorimplement する必要があるのだが、どうもそのためにこれが必要らしい。

ちなみに、これを書かなかった場合、下記のエラーとなる

Error: No value accessor for form control with name: 'xxx'

ControlValueAccessorimplement

export class TestNgModelComponent implements ControlValueAccessor {

上記のように、 ControlValueAccessorimplement する。

これにより、

writeValue()
registerOnChange()
registerOnTouch()
setDisabledState()

のメソッドの実装が必要となる。

上記の各メソッドが何を行いどんなタイミングで呼び出されるものなのか、詳細が分かる人がいたら教えて欲しい(切実)が、とりあえず名前か導かれるような動作をするんだと推測している。

とりあえず、 ngModel が動くように最低限の実装をしておく


export class TestNgModelComponent implements ControlValueAccessor {
  private onTouchedCallback: () => void = () => {};
  private onChangeCallback: (_: any) => void = () => {};

  writeValue(text: string): void {
    if (text !== this.value) {
      this.value = text;
    }
  }

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

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

  setDisabledState(isDisabled: boolean): void {}
}

valueの getter , setter を作成

最後に、 valuegettersetter を用意する

  _value: string; // value を保存しておく変数を定義する。名前はなんでも良い

  get value(): string {
    return this._value;
  }
  @Input('value')
  set value(text: string) {
    if (this._value !== text) {
      this._value = text;
      this.onChangeCallback(text);
    }
  }

これで value に対する getter , setter は用意できた。より複雑な仕様にしたい場合はこの setter 部分などを変更すると良いと思われる。

最終的なコード

コンポーネントのTypescript

@Component({
  selector: 'app-test-ng-model',
  templateUrl: './test-ng-model.component.html',
  styleUrls: ['./test-ng-model.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      multi: true,
      useExisting: forwardRef(() => TestNgModelComponent),
    },
  ],
})
export class TestNgModelComponent implements ControlValueAccessor {
  @Input() label: string;
  _value: string;
  private onTouchedCallback: () => void = () => {};
  private onChangeCallback: (_: any) => void = () => {};

  get value(): string {
    return this._value;
  }
  @Input('value')
  set value(text: string) {
    if (this._value !== text) {
      this._value = text;
      this.onChangeCallback(text);
    }
  }

  writeValue(text: string): void {
    if (text !== this.value) {
      this.value = text;
    }
  }

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

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

  setDisabledState(isDisabled: boolean): void {}
}

コンポーネントのView

<div>
  <label> {{ label }} </label>
  <input [(ngModel)]="value">
</div>

このコンポーネントを使う親のコンポーネントのView
(キーワードという文言のInputという仮定で作成)

<div class="container">
  <app-test-ng-model name="keyword" label="キーワード" [(ngModel)]="keyword"></app-test-ng-model>
</div>

最後に

Angularの公式は、こういう少し複雑なことを調べるのがかなり大変で、試行錯誤するしかない印象なので、こうやって溜めていきます。
ngModel は結構めんどうなので、 ngmodelをやるためだけの親クラスとか作ると捗るかも。