Angularでちょっとした表を表示しようとしていて、「やりたいことの変化に柔軟に対応できるよう、セルに表示する値のデータバインディングを動的に変更できたら良いな」と思ったのだが、なかなかやり方が分からなかった。調べたところ、これはAngularのテンプレートだけでは実現できず、lodashの助けが必要だとわかったので、忘れないようやり方をまとめる。
静的なデータバインディング
動的なデータバインディングの前に、普通の静的なデータバインディングだとどうなるかを示す。例えば以下のようなdata
配列に対し、配列の要素を行ごとにユーザが指定したプロパティ(例えばcoord.x
)だけを表示することを考える。表示したいデータは、下図の赤枠部分とする。
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: {{と}}で囲む)を使って以下のように書くことができる。
<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
プロパティが表示できていることが確認できる。
動的なデータバインディング
静的なデータバインディングでも表示したいものが表示できればよいが、coord.x
だけでなくcoord.y
やvalue
プロパティを、任意の順番で表示したり動的に切り替えたりしたい場合には対応できない。(ngIfをたくさん作ればある程度はできるが、あまり柔軟ではないし、ドットを含んだ時点で扱いが難しくなる)
動的なデータバインディング(動的と言っていいのかどうか分からないが、とりあえずそう呼ぶ)をするには、あるオブジェクトのプロパティにパス(coord.x
のような文字列)を使ってアクセスできる必要がある。これができれば、パスを変更することでバインディング対象のプロパティを変更することができる。こうしたアクセスはlodashのget
とset
関数で実現できる。(lodash: get, set)
lodashのget/set
オブジェクトのプロパティへ、プロパティまでのパス(文字列)からアクセスできるようになる関数。例えば以下のように、ドット記法を使ったプロパティへのアクセスと同じことを、パスを使ってできるようになる。(その分処理負荷は上がるかと思う)
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
を使って元のオブジェクトに反映する。
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
に表示する値を切り替えることができる。
@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();
}
}
テンプレートは以下のようにして、選択したパスに対するプロパティだけを表示するようにする。
<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>
動かした結果
クリックするたびに矢印のように表示が切り替わることが確認できた。
まとめ
動的にデータバインディングしたいプロパティを変えたいときは、オブジェクトのプロパティへ「パス」を使ってアクセスできるlodashのget
とset
を使う。ただし、通常のデータバインディングと違って、オブジェクトを渡しているので、変更検知が意図通り動くかどうかには気を付ける。