Angularで動的なデータバインディングを作る

Angularでちょっとした表を表示しようとしていて、「やりたいことの変化に柔軟に対応できるよう、セルに表示する値のデータバインディングを動的に変更できたら良いな」と思ったのだが、なかなかやり方が分からなかった。調べたところ、これはAngularのテンプレートだけでは実現できず、[lodash](https://lodash.com/docs/4.17.15)の助けが必要だとわかったので、忘れないようやり方をまとめる。

#静的なデータバインディング
動的なデータバインディングの前に、普通の静的なデータバインディングだとどうなるかを示す。例えば以下のような`data`配列に対し、配列の要素を行ごとにユーザが指定したプロパティ(例えば`coord.x`)だけを表示することを考える。表示したいデータは、下図の赤枠部分とする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/ced177b0-f8a1-d7a9-60cc-ca5057163cc0.png)

```typescript:sample.component.ts
interface SampleData {
  coord: { x: number, y: number };
  value: number;
}

export class SampleComponent {
  ...略...
  readonly data: SampleData[] = [
    { coord: { x: 1, y: 10 }, value: 100 },
    { coord: { x: 2, y: 20 }, value: 200 },
    { coord: { x: 3, y: 30 }, value: 300 },
    { coord: { x: 4, y: 40 }, value: 400 },
  ];
  ...略...
```

表示したいプロパティは変わらないので、Angularのテンプレートで補間(Interpolation: {{と}}で囲む)を使って以下のように書くことができる。

```html:sample.component.html
<p> Current Data: {{ data | json }}</p>
<div *ngFor="let d of data; index as i">
<span>data[{{ i }}].coord.x: </span>
<input [(value)]="d.coord.x">
</div>
```
表示してみると、行ごとに`coord.x`プロパティが表示できていることが確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/32c33cf1-8334-ef8c-b4c9-e85a6e86e751.png)

#動的なデータバインディング
静的なデータバインディングでも表示したいものが表示できればよいが、`coord.x`だけでなく`coord.y`や`value`プロパティを、任意の順番で表示したり動的に切り替えたりしたい場合には対応できない。(ngIfをたくさん作ればある程度はできるが、あまり柔軟ではないし、ドットを含んだ時点で扱いが難しくなる)

動的なデータバインディング(動的と言っていいのかどうか分からないが、とりあえずそう呼ぶ)をするには、あるオブジェクトのプロパティにパス(`coord.x`のような文字列)を使ってアクセスできる必要がある。これができれば、パスを変更することでバインディング対象のプロパティを変更することができる。こうしたアクセスはlodashの`get`と`set`関数で実現できる。(lodash: [get](https://lodash.com/docs/4.17.15#get), [set](https://lodash.com/docs/4.17.15#set))

## lodashのget/set
オブジェクトのプロパティへ、プロパティまでのパス(文字列)からアクセスできるようになる関数。例えば以下のように、ドット記法を使ったプロパティへのアクセスと同じことを、パスを使ってできるようになる。(その分処理負荷は上がるかと思う)

```typescript
import { get, set } from 'lodash';

let x;
// プロパティ coord.x の取得
// プロパティ coord.y の設定

// Typescript(Javascript)の書き方
x = data[0].coord.x;
data[0].coord.y = 1;

// lodashでパスを使った書き方
x = get( data[0], 'coord.x' );
set( data[0], 'coord.y', 1 )
```

## lodashを使った動的なデータバインディングの例
lodashを使って、動的なデータバインディングをしてみる。例えば`Input`要素に、`data`で与えられたオブジェクトの`binding`で与えられたパスのプロパティを表示し、値が変更されたら反映する単純なコンポーネント`CellComponent`は以下のようになる。

`Input`要素へ値を反映するときは、lodashの`get`を使って、オブジェクトからプロパティを取得して`Input`要素の`value`プロパティ渡す。`Input`要素の値が変化した時(`valueChange`イベント)は、lodashの`set`を使って元のオブジェクトに反映する。また、`Input`要素の`input`イベントが発生した時は、新しい文字列(`$event.target.value`)をlodashの`set`を使って元のオブジェクトに反映する。

```typescript
import { get, set } from 'lodash';

@Component({
  selector: 'cell',
  template: `<input [value]="get()" (onValueChange)="set($event)" (input)="set($event.target.value)"/>`
})
export class CellComponent {
  @Input() data: any;
  @Input() binding: string;
  get() {
    return get( this.data, this.binding );
  }
  
  set( value: number ) {
    set( this.data, this.binding, value );
  }
}
```

## 動的なバインディングのを動かしてみる
あまり実用的ではないが、配列内のすべてのオブジェクトについて、パスで指定したプロパティを`Input`要素に表示するコンポーネント`DynamicBindingComponent`を作ってみる。このコンポーネントでは、ボタンをクリックするたびに`changeBinding()`でパスを`coord.x`, `coord.y`, `value`の順に切り替えることで、`Input`に表示する値を切り替えることができる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/65749396-d77d-2d04-f3e5-716c775c30bf.png)

```typescript:dynamic-binding.component.ts
@Component({
  selector: 'dynamic-binding',
  templateUrl: './dynamic-binding.component.html',
  styleUrls: ['./dynamic-binding.component.scss']
})
export class DynamicBindingComponent{
  // 表示したいデータ
  readonly data: SampleData[] = [
    { coord: { x: 1, y: 10 }, value: 100 },
    { coord: { x: 2, y: 20 }, value: 200 },
    { coord: { x: 3, y: 30 }, value: 300 },
    { coord: { x: 4, y: 40 }, value: 400 },
  ];

  // バインディング対象のプロパティを示すパス(3種)
  readonly bindings = [ 'coord.x', 'coord.y', 'value' ];

  // バインディングに使用するパス
  index: number = 0;
  binding: string;
  
  // バインディングに使用するパスを変更する(bindings内のパスを順番に回す)
  changeBinding() {
    this.index++;
    this.binding = this.bindings[ this.index % this.bindings.length ];
  }

  constructor() {
    this.changeBinding();
  }
}
```
テンプレートは以下のようにして、選択したパスに対するプロパティだけを表示するようにする。

```html:dynamic-binding.component.html
<p> Current Data: {{ data | json }}</p>
<button (click)="changeBinding()">Change Binding</button>
<div *ngFor="let d of data; index as i">
<span>data[{{ i }}].{{ binding }}: </span>
<cell [binding]="binding" [data]="d" ></cell>
</div>
```

### 動かした結果
クリックするたびに矢印のように表示が切り替わることが確認できた。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296170/5b41f318-bc1c-6899-3664-6fe32a4ba691.png)

#まとめ
動的にデータバインディングしたいプロパティを変えたいときは、オブジェクトのプロパティへ「パス」を使ってアクセスできるlodashの`get`と`set`を使う。ただし、通常のデータバインディングと違って、オブジェクトを渡しているので、変更検知が意図通り動くかどうかには気を付ける。