notebook

都内でWEB系エンジニアやってます。

独自コンポーネントでフォーム用の部品を作る(ngModelを使えるようにする)

独自のコンポーネントでも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の値が反映されている状態になっています

f:id:swfz:20171017025421p:plain

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

f:id:swfz:20171017025433p:plain

まとめ

今までControlValueAccessorの存在を知らずに無理やりカスタムコンポーネント作って@Input,@Outputを駆使してごにょごにょやっていたのでどうしてもコード量も増えるし読みづらくなるしでどうしたものかと思っていたのですがこれで解決できそうです

同時に負債を量産していたことに気づきました。。。

angular wayに乗っかることで独自実装みたいなのを減らせそうなのでメンテの観点からもメリットがあるなと思いました

てか入門書とかに書いておいてもいいんじゃないかと思いました

angularを学び初めて思うのはこういうinterfaceを知ってるだけで全然違うなーということですね、痛感してます