Angular  MaterialのWrapper Componentを作る

こんにちは。24日目、思いっきり遅刻してしまい申し訳ありません。Angularを使い始めて半年も経ってないですが、Angular Material ComponentのWrapper component（特にmat-radio-button）を作りながらいくつか気づいたところを書いていきたいと思います。

# `@Input`
まず基本的なInputプロパティ。普通のやり方で`disabled`で例えると

- Wrapperを使うクライアントからのBinding

```html:app.component.html
<app-radio-button [disabled]="true">Option　1</app-radio-button>
```
- WrapperのInputプロパティを経由して

```ts:radio-button.component.ts
@Component({
...
})
export class RadioButtonComponent {
  @Input()
  public disabled: boolean;
...
```
- WrapされるComponentプロパティにBinding

```html:radio-button.component.html
<mat-radio-button [disabled]="disabled">
  <ng-content></ng-content>
</mat-radio-button>
```
の順番にデータが流れます。

## HTML attribute対応
```html:app.component.html
<app-radio-button disabled>Option　1</app-radio-button>
```
ここで上のように`disabled`をHTML attributeにも対応するには
（ここで`disalbed`は空の文字列が入る）

```ts:radio-button.component.ts
...
export class RadioButtonComponent {
  private _disabled: boolean;
  @Input()
  get disabled(): boolean { return this._disabled; }
  set disabled(value) {
    this._disabled = (value === '' || !!value);
  }
...
```
のように強引にstring型にも対応した感じですが、実は**Angular Material側ですでに対策済み**であるため、ただプロパティを流すだけでHTML attributeにも対応ができてしまいます。(というか上の方法ではTSエラーになってしまいます。)

### `coerceBooleanProperty()`
対策されているAngular Materialを覗くと下記のようにCDKのヘルパー関数が使われています。

```ts:radio-button.component.ts
import { coerceBooleanProperty } from '@angular/cdk/coercion';
...
  set disabled(value) {
    this._disabled = coerceBooleanProperty(value);
  }
...
```
HTML attributeで取得される`value`はstring型になり、上記の関数は`value`が文字列なら`'false'`以外すべてを`true`に変換するようです。
もしプロパティの行き先がAngular MaterialのComponentじゃないときに使うと便利そうですね。

# mat-radio-group
　Front-end開発において、Radio buttonはその性質上、複数のComponentで単一のフォームを持つ必要があります。ReactiveFormsだったら`FormControl`、Template-drivenだったら`NgModel`を**どのタグに持たせるか**。Angular Materialでは`<mat-radio-group>`という親タグその役割を担っています。
　しかし、Radio buttonはまた、他のinputフォームと違って、name属性によってGroupingされる特殊な仕様があります。各`radio-button`タグでnameを指定してまとめる手もありますが、そうしてしまうと親の`radio-group`タグの存在意義がなくなる。
　これをどう対処するか悩ましいところですが、そもそもwrapしようとするmat-radio-groupを覗くと、

```html:material2/src/lib/radio/radio.html
    <input #input class="...
        ...
        [attr.name]="name"
```
```ts:material2/src/lib/radio/radio.ts
...
let nextUniqueId = 0;
...
@Directive({
...
})
export class MatRadioGroup extends ... {
  ...
  // nameはgroupコンポーネントで決まる
  private _name: string = `mat-radio-group-${nextUniqueId++}`;
  ...
  // content内の子コンポーネントのリストを取得
  @ContentChildren(forwardRef(() => MatRadioButton), { descendants: true })
  _radios: QueryList<MatRadioButton>;
  ...
  @Input()
  get name(): string { return this._name; }
  set name(value: string) {
    this._name = value;
    // nameが変わったら子コンポーネントにも反映する
    this._updateRadioButtonNames();
  }
  ...
  private _updateRadioButtonNames(): void {
    if (this._radios) {
      this._radios.forEach(radio => {
        radio.name = this.name;
      });
    }
  }

```
なるほど、`@ContentChildren`で子コンポーネントを取得し、親と同じnameをセットする仕組みのようです。

　今度は`MatRadioButton`を見ると、

```ts:material2/src/lib/radio/radio.ts
...
@Component({
...
})
export class MatRadioButton extends ... implements OnInit, ... {
  ...
  @Input() name: string;
  ...
  constructor(
    @Optional() radioGroup: MatRadioGroup,
    ...
  ) {
    ...
  }
  ...
  ngOnInit() {
    if (this.radioGroup) {
      ...
      // 親groupのnameを引き継ぐ
      this.name = this.radioGroup.name;
    }
  }
...
```
DIで親コンポーネントを取得してそのnameを引き継ぎます。

## Circular dependency
　ソースを見て気づいた方も多いと思いますが、`MatRadioButton`と`MatRadioGroup`が自分のクラスの中で互いを参照する状態になっています。上記の例のように`radio.ts`という一つのファイルの中なら`forwardRef`を使うだけでなんとかなっています。ずるい。
　しかし、Wrapper Componentを作る際はAngular流儀上、１コンポーネント/１ファイルになるため、そのまま真似してしまうと、

```console
WARNING in Circular dependency detected:
src/app/radio-button/radio-button.component.ts -> src/app/radio-group/radio-group.component.ts -> src/app/radio-button/radio-button.component.ts

WARNING in Circular dependency detected:
src/app/radio-group/radio-group.component.ts -> src/app/radio-button/radio-button.component.ts -> src/app/radio-group/radio-group.component.ts
```
を食らってしまうし、一度決まったnameをプログラム途中で変更するユースケースは想定しにくいので、

- ButtonからGroupのnameを取りに行く
- Groupからは何もしない(Buttonを参照しない)

の方針で、依存をButton(子)→Group(親)のように一方向にするのが妥当かなと思います。

# ControlValueAccessor
　Custom Form Controlを作るために`ControlValueAccessor`を実装します。`ControlValueAccessor`とはAngular Form API(FormControl)とDOMのNative(or Custom) Elementをつなぐinterfaceで、`<input>`などフォームタグ(Native element)に対応するDirectiveはすべてこのinterfaceを実装しており、以下のように定義されています。

|              Accessor              |     Form Element     |
|------------------------------------|----------------------|
| DefaultValueAccessor               | input, textarea      |
| CheckboxControlValueAccessor       | input[type=checkbox] |
| NumberValueAccessor                | input[type=number]   |
| RadioControlValueAccessor          | input[type=radio]    |
| RangeValueAccessor                 | input[type=range]    |
| SelectControlValueAccessor         | select               |
| SelectMultipleControlValueAccessor | select[multiple]     |

`ControlValueAccessor`の各メソッドの実装が済んだら、あとは`NG_VALUE_ACCESSOR`というDI tokenを持ってProviderに登録することで、FormControlが使えるようになります。

```ts:radio-group.component.ts
@Component({
  ...
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => RadioGroupComponent),
    multi: true
  }],
  ...
})
export class RadioGroupComponent implements ControlValueAccessor, ... {
  ...
  writeValue(obj: any) {...}
  registerOnChange(fn: any) {...}
  registerOnTouched(fn: any) {...}
  setDisabledState(isDisabled: boolean) {...}
}
```
DI tokenとかProviderとか調べるとどういう仕組になってるのか、あれこれ説明されているところは多いですが、いくらソースコードを読んでみても、そもそもの**AngularのDIコンテナの仕組み**自体あまり理解できず、雰囲気だけで作ってる感があるので、詳しく説明していただける方がいらっしゃれば…


# References
- [Never again be confused when implementing ControlValueAccessor in Angular forms](https://blog.angularindepth.com/never-again-be-confused-when-implementing-controlvalueaccessor-in-angular-forms-93b9eee9ee83)
- [CUSTOM FORM CONTROLS IN ANGULAR](https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html)
- [MULTI PROVIDERS IN ANGULAR](https://blog.thoughtram.io/angular2/2015/11/23/multi-providers-in-angular-2.html)