Vueで3値を取れるCheckboxなコンポーネント書いた

ある日、チェックの有無で検索条件に含めるロジックを書く必要があり、「含める」「含めない」の2値の表現でcheckboxを使っていたが、これだと「指定しない」という条件が表現できないので、やっぱ3値欲しいなぁ、となった。

まず既存に無いか探してみたが、まともなものはなかった。
標準でうまくやれるようにやってくれてるんじゃないか、と思って調べたが、issueからそういうのはコンポーネントでやれ、ということだった。
https://github.com/vuejs/vue/issues/4094

というわけで、書きました。codesandboxいいですね・・・。

Demo: https://codesandbox.io/s/qqzrwlx8pj

### 特徴

- indeterminateで値を取れる(デフォルトはundefined)
- **indeterminateに切り替えできる**
- v-model OK
- 複数選択は一応出来る（このせいで大分コードが増えてる）


### 問題点

問題点というか、多分良くない対処をしている箇所。

- 切り替え順序の問題
- 複数選択時の初期値とフォームの状態の同期


以降、メモ書き
-------------


まとも(?)なカスタムコンポーネントを書くのは初めてだったのもあり、結構難産だった。なので、以降は苦労したところをダラダラと書いていく。


### indeterminateに切り替えについて

ブラウザの標準のindeterminateは初期値としてしか使えず、後はon/offしかできない。on/off/indeterminateを循環して切り替えできないと意味がなかったので書いている。

切り替えの順序に依存する問題については、循環する順序が「on→off→indeterminate→on」なら問題ないが「off→on→indeterminate→off」という順序だと動かない感じになっている。ブラウザはindeterminateを優先するっぽいのと、状態遷移についてindeterminate→on→off→on→off...というようになっており、indeterminate→offに切り替えようとするのとうまくマッチしない感じなんだと思う。vueのcomputedで計算して適切な形になると思ってたが、うまくいかない。


### v-modelの対応について

[v-modelは独自のコンポーネントにも使えて](https://jp.vuejs.org/v2/guide/components.html#%E3%82%B3%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%8D%E3%83%B3%E3%83%88%E3%81%A7-v-model-%E3%82%92%E4%BD%BF%E3%81%86)、使えるようにするのも簡単。やる事は単純で、`props`で`value`を受け取り、変更したくなったら`this.$emit('input', newValue)`とすればよい。
なお、[標準のフォームはわりと特別扱いしている](https://github.com/vuejs/vue/blob/2.6/src/platforms/web/compiler/directives/model.js#L37-L52)。[`checkbox`で`true-value`が扱える](https://jp.vuejs.org/v2/guide/forms.html#%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9-1)のも特別扱いのおかげ。これを参考にしました。

そうは言っても動くコードで実装例を見たい、という場合、[テストコード](https://github.com/vuejs/vue/blob/2.6/test/unit/features/directives/model-component.spec.js)が割と良かったです。

### 複数選択について

複数選択というのはArrayを与えると複数のチェックボックスを1つのフィールドで管理できるようになる機能で、[Vueのv-modelも標準で対応している](https://jp.vuejs.org/v2/guide/forms.html#%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%83%9C%E3%83%83%E3%82%AF%E3%82%B9)。
しかし標準のほうは、Arrayの場合、valueの有無を設定するので、任意の値を設定する`true-value`,`false-value`が機能しない。（[jsfiddle](https://jsfiddle.net/nhs61bvy/4/)）
これ、アンドキュメントな動作なので、引っかかってました。難しい・・・

一方で複数選択可能かつ`true-value`,`false-value`を機能させるの「そんな難しくないだろーついでに対応するか」と思い触り始めたら`true-value`に`Array`が入ると区別できなくなることが分かったので、使えるようにする代わりに明示的にmultiple属性を指定させるようにして妥協した。

### 初期値と見た目の同期について

初期値と見た目の同期についての問題について、そもそも初期値がわからない、というケースは無くはない。
indeterminateが値を持つというのが不自然では？というのもあるが、そうは言っても値を設定したい時はあると思っていて、じゃあ設定できるようにしてみよう、とした。
が、じゃあ初期値が無い状態はどうなるの？という問題がある。indeterminateで指定された値も指定されていない場合は何を指定すればよいのか？
というわけで、初期値が無い状態はindeterminateに合わせて初期化してあげれば良いか、という雑な対応をすることにした。

ちなみに、状態を判別するために比較する必要があるのと、値は同一オブジェクトとは限りません。
だから[deepEqualsを用意する](https://qiita.com/fukasawah/items/d365099a3220bf127491)必要があったんですね。


### 複数選択の初期値化

前述の同期の影響というのもあるのだけど、複数選択ではさらに問題があり、その回避のためsetTimeoutを使っている箇所がある。

初期表示時はn個の要素から同時に同じpropsの値を使おうとする。で、最初の要素で値を更新(emit)しても、他の要素では更新前の値を使って更新(emit)するので後勝ちになる、という問題があった。emitを使わずpropsを直接いじれば見た目上は動くが、確かそれはやっちゃダメな事例だったはず。

・・・と、文字で説明するのは分かりにくいのでデモを用意した。

setTimeout無し: https://jsfiddle.net/nhs61bvy/5/ (1が表示される)
setTimeout有り: https://jsfiddle.net/nhs61bvy/6/ (3が表示される)
props直接弄り: https://jsfiddle.net/nhs61bvy/7/ （3が表示される）

要はこう言った事が起きてるのでsetTimeout使いました、ということです。多分対応としては良くないはずなので、もうちょっと考えたい。