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

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

# 今回作るコンポーネント
label+inputという簡単なコンポーネント


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

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

# ngModel を独自コンポーネントで実装する手順
最初に、手順をまとめると、下記のようになります。

1. `@Component` に `provider` を追加
2. `ControlValueAccessor` を implement
3. valueの `getter` , `setter` を作成

## `@Component` に `provider` を追加
```ts

@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` を利用するには、 `ControlValueAccessor` を `implement` する必要があるのだが、どうもそのためにこれが必要らしい。

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

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

## `ControlValueAccessor` を `implement`

```ts
export class TestNgModelComponent implements ControlValueAccessor {
```
上記のように、 `ControlValueAccessor` を `implement` する。

これにより、 

```ts
writeValue()
registerOnChange()
registerOnTouch()
setDisabledState()
```
のメソッドの実装が必要となる。

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

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

```ts

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` を作成
最後に、 `value` の `getter` と `setter` を用意する

``` ts
  _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

``` ts
@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

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

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

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

# 最後に

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