独自のコンポーネントでもngModelを使ってデータバインディングをしたい
ngModelを使うことでvalidationなども他のinputなどと同様に扱いたい
ということで公式のドキュメントにも乗っているのでやってみます
angular4で実装してます
Angular - ControlValueAccessor
https://angular.io/api/forms/ControlValueAccessorangular.io
- ControlValueAccessor
 
このinterfaceをimplementsして実装すればいいようです
各メソッドの中身を少し見て見ます
writeValue
model -> viewの変更時
formのAPIがviewに書き込むために呼び出される
ngModelで初期値とかを渡す場合valueにその値が入ってきてwriteValueが呼ばれるようです
registerOnChange
fnに渡されたものをのぞいてみるとこんな感じでした
ƒ (newValue) {
        dir.viewToModelUpdate(newValue);
        control.markAsDirty();
        control.setValue(newValue, { emitModelToViewChange: false });
    }
フォームの値の変更を呼び出し側に通知するための処理が書いてあります
control経由で呼び出し側のvalueを変更するみたいですね
registerOnTouched
ƒ () { return control.markAsTouched(); }
こちらはtouchされたことをcontrol経由で呼び出し側に通知するための処理が書かれているようです
要はblurのようです
試しに作ってみる
今回はよくあるレーティングを選択するUIを作ってみます
- custom-selector.component.ts
 
import { Component, OnInit, Input, forwardRef } from '@angular/core';
import {ControlValueAccessor,NG_VALUE_ACCESSOR} from '@angular/forms';
export const CUSTOM_SELECTOR_CONTROL_VALUE_ACCESSOR = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => CustomSelectorComponent),
  multi: true,
};
@Component({
  selector: 'custom-selector',
  templateUrl: './custom-selector.component.html',
  styleUrls: ['./custom-selector.component.scss'],
  providers: [CUSTOM_SELECTOR_CONTROL_VALUE_ACCESSOR],
})
export class CustomSelectorComponent implements OnInit , ControlValueAccessor {
  constructor() { }
  @Input() max: number = 5;
  private _onChange: any = Function.prototype;
  private _onTouched: any = Function.prototype;
  public ranges: any = [];
  public value: number;
  ngOnInit() {
    this.ranges = Array.from(Array(this.max).keys());
  }
  writeValue(value: any): void {
    this.value = value;
  }
  registerOnChange(fn: (_: any) => {} ): void {
    this._onChange = fn;
  }
  registerOnTouched(fn: () => {} ): void {
    this._onTouched = fn;
  }
  setRate(rate: number){
    this.writeValue(rate);
    this._onChange(this.value);
    this._onTouched();
  }
}
@Input() max: number = 5で最大数のデフォルトを5にしているのと呼び出す側が指定できるようにしています
- custom-selector.component.html
 
<ng-container *ngFor="let rate of ranges; let i = index;">
  <span (click)="setRate(rate + 1)">
    <i [ngClass]="i < value ?['fa','fa-star'] : ['fa', 'fa-star-o']"></i>
  </span>
</ng-container>
<div>
  custom component: {{value}}
</div>
- app.component.html
 
呼び出し側のコードはこんな感じ
<custom-selector [(ngModel)]="rate"
                 #custom="ngModel"
                 [max]="10">
</custom-selector>
<div>
  touched: {{custom.touched}}
</div>
<div>
  parent component: {{rate}}
</div>
こんな感じになりました
呼び出し側でrateの初期値を2にしているのでそれがカスタムコンポーネントに渡って2の値が反映されている状態になっています

クリックすると値が変更されているのとcontrolのtouchedが変更されたことがわかります

まとめ
今までControlValueAccessorの存在を知らずに無理やりカスタムコンポーネント作って@Input,@Outputを駆使してごにょごにょやっていたのでどうしてもコード量も増えるし読みづらくなるしでどうしたものかと思っていたのですがこれで解決できそうです
同時に負債を量産していたことに気づきました。。。
angular wayに乗っかることで独自実装みたいなのを減らせそうなのでメンテの観点からもメリットがあるなと思いました
てか入門書とかに書いておいてもいいんじゃないかと思いました
angularを学び初めて思うのはこういうinterfaceを知ってるだけで全然違うなーということですね、痛感してます