Vue2系と3系で微妙に挙動が違うので、備忘録として。
・v-model
便利すぎワロタ
Vueで何とも便利なのがv-model
とかいう機能である。これを書くだけで勝手に双方向データバインディングをやってくれるので面倒なことを考えなくて済む。
これは、かなり単純化された理解としては、 v-bind:value
とv-on:input
に関するシンタックスシュガー(糖衣構文)である: つまり
<input v-model="foo" />
は、以下の記述と動作的には等価である。
<input
:value="foo"
@input="foo = $event.target.value"
>
input
要素のtype
が違う場合にはバインドされるプロパティが変わる(checkbox
やradio
なら:value
の代わりに:checked
のように)が、展開される中身は同じである。
・複数ある時は?
ところが、これは単一のinput
要素に対してv-model
でバインドするときの話で、複数のチェックボックスに対して同じv-model
をバインドするときには話が変わってくる。試しに上で示したような "等価な" input
要素を複数並べてみれば、全然動かないことはすぐに明らかになるはずだ。
実際に複数のチェックボックスに対して同一のv-model
をバインドするときには、バインドされる変数は配列であるので、実際に等価な内容に展開しようとすると以下のようになるはずである。
<input type="checkbox" :value="thisValue" v-model="bindValue">
//上のものと同じ動作をする冗長な記述
<input
type="checkbox"
:value="thisValue"
:checked="bindValue.includes(thisValue)"
@input="bindValue = bindValue.includes($event.target.value) ?
bindValue.filter((v) => v !== $event.target.value) :
bindValue.concat($event.target.value)
"
>
つまりバインドされた変数 ("bindValue"
)は、どの要素にチェックが入っているかを表す配列であって、checked
属性は配列に値が含まれているかで判断するし、input
イベントの際には配列に値を出したり入れたりしなければならないということである。
このようにv-model
はその要素のタイプや数によって展開される内容は大きく変わってくるし、そしてコンポーネント化する際にはそれを十分に把握しておく必要がある。
・コンポーネント化してみる
さて、カスタムのコンポーネントにv-model
を使う場合であるが、特にtype
を指定しなければ、Vue2系なら、バインドされる属性は(冒頭に示した例のように):value
と@input
である。(Vue3系の話は後でする。本質的には同じだからとりあえず置いておく)
であるからには、コンポーネントはprops
としてvalue
を持っておき、(コンポーネントはprops
で受け取った値を直接変更できないから)input
イベントをemit
すればよいはずである。
したがって実直にいくならば、先ほど掲げた等価な展開を多少弄って下のようにすればよい。
<template>
<div class="multi-checkbox">
<input
type="checkbox"
:checked="value.includes(thisValue)"
@input="(event) =>{
$emit('input', value.includes(event.target.value) ?
value.filter(v => v !== event.target.value) :
value.concat(event.target.value)
)
}"
:id="id"
:value="thisValue"
>
<slot />
</div>
</template>
<script>
export default {
props: ['value', 'id', 'thisValue']
}
</script>
変わっているのは、@input
の中身が直接値を書き換えるのではなくて、input
イベントをemit
していることだけである。
コンポーネントを使う側は普通にv-model
を使えばよい。ただしコンポーネント内のinput
要素に渡す必要がある:value
属性の値はprops
経由だから、そこは気を付ける必要がある。
<template>
<div>
<multi-checkbox v-model="checkList" :id="0" :thisValue="0">hoge</multi-checkbox>
<multi-checkbox v-model="checkList" :id="1" :thisValue="1">fuga</multi-checkbox>
<multi-checkbox v-model="checkList" :id="2" :thisValue="2">piyo</multi-checkbox>
...
</div>
</template>
<script>
import MultiCheckbox from "./components/MultiCheckbox.vue"
export default {
components: { MultiCheckbox },
data() {
return {
checkList: []
}
}
}
</script>
ただし、これにはもっと簡単な方法があって、それはコンポーネントの中でもv-model
を使うというものである。そのままv-model
を使ってもうまくいかないが、v-model
にバインドされた変数をget/set
するプロパティを作成して、そのsetter
の中でイベントをemit
することで
<template>
<div class="multi-checkbox-prop">
<input type="checkbox" v-model="bindProp" :id="id" :value="thisValue" />
<slot />
</div>
</template>
<script>
export default {
props: ["value", "id", "thisValue"],
computed:{
bindProp:{
get(){ return this.value },
set(newValue) { this.$emit('input', newValue) }
}
}
}
</script>
のように書くことができる。これを使えばわざわざ冗長に展開された"等価"な処理を書かずとも、厄介な処理を全部v-model
に放り投げることができる。実用的にはこちらが一番だろう。
・Vue3のときは?
前節は、Vue2系のときは、と念押ししておいたが、Vue3系の時でも本質的には同じである。ただし微妙な仕様の変更により、バインドされるイベント・属性が変わっているので、そこだけ抑えればよろしい。
具体的には、Vue2系では(通常のinput
要素と同様に):value
と@input
だったものが、:modelValue
と@update:modelValue
に変更されている。したがって上で掲げたコンポーネントの記述のうちvalue
をmodelValue
に、input
をupdate:modelValue
に置き換えればよいだけである。
なおVue3からはコンポーネントがemit
するイベントをemits
オプションで宣言することが推奨されているから、props
の下にemits: ['update:modelValue']
を追記しておくとなお良いだろう。なくても動くが。
今回はVue SFC Playgroundで好きに弄って遊べるようにしてみたので興味があれば行ってみて実際に動いているか確認したり改変してみて試してみるとよいだろう。ただしVue3系のみである。一応これと同じソースコードはgistに置いてあるから、ローカル環境で触りたい人はこちらからダウンロードするとよい。
jsは嫌いだがこういう実装を追うのは言語に依らず好きだ。jsは嫌いだが。