AngularJS で select タグを思い通りに作る


おそらく当たり前のことなんだろうけど、自分が調べた際に色んなページを見て回ることになったので、備忘を兼ねて。

あと、そもそも何か勘違いしていて、このケースに合うより良い書き方があるのであれば、教えていただけると幸いです。

# やりたいこと

以下の形の select タグを生成する。

```html
<select>
	<option value="1">プロジェクトA</option>
	<option value="2">プロジェクトB</option>
	<option value="3">プロジェクトC</option>
	<option value="11">プロジェクトK</option>
	<option value="20" selected="selected">プロジェクトT</option>
	<option value="26">プロジェクトZ</option>
</select>
```

サーバからは以下のデータが送られてくるものとする。

```rb
current = 20
pj_list = {
	1  : 'プロジェクトA',
	2  : 'プロジェクトB',
	3  : 'プロジェクトC',
	11 : 'プロジェクトK',
	20 : 'プロジェクトT',
	26 : 'プロジェクトZ'
}
```

補足として、以下の制約があるものとする。

1. プロジェクトTが初期値として選択されている状態
1. プロジェクトの表示順は pj_list のキーの昇順
1. option value には pj_list のキーがセットされる
1. option のラベルには pj_list の値がセットされる

これを実現するのに、try and error を繰り返すことになった。

# 普通に書いてみた

```html
<select ng-model="current" ng-options="key as val for (key, val) in pj_list">
```

出力は以下のようになった。

```html
<select ng-model="current" ng-options="key as val for (key, val) in pj_list">
	<option value="?" selected="selected"></option>
	<option value="1">プロジェクトA</option>
	<option value="11">プロジェクトK</option>
	<option value="2">プロジェクトB</option>
	<option value="20">プロジェクトT</option>
	<option value="26">プロジェクトZ</option>
	<option value="3">プロジェクトC</option>
</select>
```

この結果は期待するものとは大きく異なる。具体的には、制約の1番と2番が満たされていない。

# 型を気にしてみた

`current` に入っている `20` という値がそのままなのがまずかったかと考え、値を `'20'` にしてみた(数値から文字列に変換)。

結果、出力は以下のようになった。

```html
<select ng-model="current" ng-options="key as val for (key, val) in pj_list">
	<option value="1">プロジェクトA</option>
	<option value="11">プロジェクトK</option>
	<option value="2">プロジェクトB</option>
	<option value="20" selected="selected">プロジェクトT</option>
	<option value="26">プロジェクトZ</option>
	<option value="3">プロジェクトC</option>
</select>
```

変更した点について、期待通りの結果が得られた。後はソート順をよしなにするだけ。

# 順序を指定するため、データの構造を変えてみた

`ng-options` ( `ng-repeat` も)はオブジェクトをキーの昇順で勝手に並べ変えてくれるように見える。ただしソート順は文字列の昇順。

今回は数値の昇順としてソートされてほしかったため、このままでは使えなかった。

色々調べた結果、オブジェクト形式ではなくオブジェクトの配列にする必要がありそう、とのことだったのでデータの構造を変更した。

```rb
current = '20'
pj_list = [
	{ key : 1,  value : 'プロジェクトA' },
	{ key : 2,  value : 'プロジェクトB' },
	{ key : 3,  value : 'プロジェクトC' },
	{ key : 11, value : 'プロジェクトK' },
	{ key : 20, value : 'プロジェクトT' },
	{ key : 26, value : 'プロジェクトZ' }
]
```

(ちなみに、データ構造を加工する処理はサーバサイドでやりたくなかったので `ng-init` のタイミングでサーバからデータを受け取ってから加工した)

データ構造の変更にあわせて、HTMl側も修正。

```html
<select ng-model="current" ng-options="pj.key as pj.value for pj in pj_list">
```

出力は以下のようになった。

```html
<select ng-model="current" ng-options="pj.key as pj.value for pj in pj_list">
	<option value="?" selected="selected"></option>
	<option value="0">プロジェクトA</option>
	<option value="1">プロジェクトB</option>
	<option value="2">プロジェクトC</option>
	<option value="3">プロジェクトK</option>
	<option value="4">プロジェクトT</option>
	<option value="5">プロジェクトZ</option>
</select>
```

一進一退、いや一進二退か。ソートは期待通りになったが、1番の他に3番も満たされなくなった。

とはいっても、1番については `current` を数値型に戻したところ上手くいった。

次の問題は value の値だ。

# 追跡式を指定してみた

リストがオブジェクトの配列として渡される場合、 `track by` でオブジェクトの識別ができるらしい(このあたり、ドキュメント読んでもピンとこなかった)。

データはこんな感じ。

```rb
current = 20 #数値に戻した
pj_list = [
	{ key : 1,  value : 'プロジェクトA' },
	{ key : 2,  value : 'プロジェクトB' },
	{ key : 3,  value : 'プロジェクトC' },
	{ key : 11, value : 'プロジェクトK' },
	{ key : 20, value : 'プロジェクトT' },
	{ key : 26, value : 'プロジェクトZ' }
]
```

タグは以下のように。

```html
<select ng-model="current" ng-options="pj.key as pj.value for pj in pj_list track by pj.key">
```

出力は以下のようになった。

```html
<select ng-model="current" ng-options="pj.key as pj.value for pj in pj_list track by pj.key">
	<option value="?" selected="selected"></option>
	<option value="1">プロジェクトA</option>
	<option value="2">プロジェクトB</option>
	<option value="3">プロジェクトC</option>
	<option value="11">プロジェクトK</option>
	<option value="20">プロジェクトT</option>
	<option value="26">プロジェクトZ</option>
</select>
```

ソート問題が無事解決された。が、初期値の指定ができていない。一進一退、なかなか期待する出力に辿りつけない。

`current` を文字列型にしてみてもダメだった。

[ドキュメント](https://docs.angularjs.org/api/ng/directive/ngOptions)をよく読んでみると、そもそも `as` と `track by` を同じ式で使うな、と書かれている。

> Do not use `select` **`as`** and **`track by`** in the same expression. They are not designed to work together.

「一緒に使って動くようにできてない」らしい。

現状を整理すると、

- ソートのため、データ構造はオブジェクトの配列にしたい
- 初期値を設定するため、 `as` を使いたい
- value を適切にセットするため、 `track by` を使いたい
- `as` と `track by` は同時に使えない

となり、手詰まりになった。

# 解決法( `as` を使わずに初期値を設定する)

諦めきれなくて色々探し回って、[この辺](http://hideaki-t.blogspot.jp/2013/11/angular-jsng-optionsvalueoption.html)とか[この辺](http://qiita.com/akkun_choi/items/618d2d9107029ab0422e#select-%E3%81%AE-ng-options)とか読んでて気づいた。

そもそも初期値を指定するのに `as` は必ずしも必要ではない。

つまり、以下のようにタグを書いても目的を達成できるはずである。

```html
<select ng-model="current" ng-options="pj.value for pj in pj_list track by pj.key">
```

問題はデータの方だ。 `pj_list` は今のままで良いが、 `current` を変える必要がある。

オブジェクトの参照で比較するのであれば、以下の形式にすれば初期値として認識できる(と思われる)。

```rb
current = { key : 20, value : 'プロジェクトT' }
pj_list = [
	{ key : 1,  value : 'プロジェクトA' },
	{ key : 2,  value : 'プロジェクトB' },
	{ key : 3,  value : 'プロジェクトC' },
	{ key : 11, value : 'プロジェクトK' },
	{ key : 20, value : 'プロジェクトT' },
	{ key : 26, value : 'プロジェクトZ' }
]
```

(実際には、 `pj_list` を加工した後、リストの中からキーが `current` とマッチするものを探してきて `current` に代入するように実装した)

結果は以下のようになる。

```html
<select ng-model="current" ng-options="pj.value for pj in pj_list track by pj.key">
	<option value="1">プロジェクトA</option>
	<option value="2">プロジェクトB</option>
	<option value="3">プロジェクトC</option>
	<option value="11">プロジェクトK</option>
	<option value="20" selected="selected">プロジェクトT</option>
	<option value="26">プロジェクトZ</option>
</select>
```

目的は達成された。

# 参照したページ

https://docs.angularjs.org/api/ng/directive/ngOptions

http://js.studio-kingdom.com/angularjs/ng_directive/select

https://github.com/angular/angular.js/issues/6564

https://docs.angularjs.org/api/ng/directive/select