Vue.jsでの双方向バインディング
Vue.jsでは、v-model
を使って双方向バインディングを実現できる。
例として、3つの選択肢のチェックボックスを考えます。この例は、Vue.jsの単一コンポーネントでソースを記述しています。
v-modelを使った双方向バインディング
-
input
タグを3つ書く - scriptの
data()
内で、それぞれのinput
の状態に対応するプロパティを定義する -
input
タグにv-model
属性を追加して、対応するプロパティを記述する
<template>
<ul>
<li>
<input type="checkbox" v-model="option1" />
<label>選択肢1</label>
</li>
<li>
<input type="checkbox" v-model="option2" />
<label>選択肢2</label>
</li>
<li>
<input type="checkbox" v-model="option3" />
<label>選択肢3</label>
</li>
</ul>
</template>
<script>
export default {
data () {
return {
option1: false,
option2: false,
option3: false
}
}
}
</script>
<style scoped>
ul {
list-style: none;
}
</style>
選択肢がいっぱい問題 & 複数のUI要素を1つのプロパティに対応させたい問題
上記の例のように、選択肢が3とかであれば、それぞれの選択肢に対応するプロパティを定義してv-model
で対応付けてやれば事足ります。
では、例えば選択肢が100個あるような状況ではどうでしょう。100個分のプロパティを予め定義するのは不可能ではありませんが、書いてるうちにバカバカしくなってしまいそうです。仕様次第ですが、おそらく100個のうち数個しか実際には使われないでしょう。
「ユーザが触れたチェックボックスだけ」状態を管理したいというニーズが発生します。
また、チェックボックスを作った時に、行全体をクリッカブルにしたい場合もあります。この時問題になるのは下記です。
-
ul
やlabel
要素などはv-model
を指定できない - 1つのプロパティに対して2つのトリガ(チェックボックス自体のクリックとラベルのクリック)がある
基本的な方針としては、次のような考え方ができそうです。
- チェックボックスに対応するプロパティを1つ1つ定義するのはやめる
-
checked
のようなオブジェクトを用意して、ユーザが触ったチェックボックスに対応するプロパティをchecked
の中に追加する - 「ユーザが触ったかどうか」は
v-on:click
のようなイベントリスナを使って判定する -
v-on:click
は、特定のプロパティに対応するUI要素にそれぞれ指定する
方針が決まったところでいざ実践。
動的にリアクティブなプロパティを追加する(ダメな例)
僕がハマった例です。
単純にチェックボックスをクリックしたらselected[option] = true
のようにすれば良いと思ってやってみました。
-
input
とlabel
にそれぞれクリックイベントを追加 - クリック時に
selected
オブジェクトの中にoption1
のような要素を追加し、選択されているか否かの判定に用いる
<template>
<ul>
<li>
<input
type="checkbox"
v-model="selected.option1"
v-on:click="select('option1')" />
<label
v-on:click="select('option1')">
選択肢1
</label>
</li>
<li>
<input
type="checkbox"
v-model="selected.option2"
v-on:click="select('option2')" />
<label
v-on:click="select('option2')">
選択肢2
</label>
</li>
<li>
<input
type="checkbox"
v-model="selected.option3"
v-on:click="select('option3')" />
<label
v-on:click="select('option3')">
選択肢3
</label>
</li>
</ul>
</template>
<script>
export default {
data () {
return {
selected: {}
}
},
methods: {
select (option) {
console.log(option)
if (!this.selected[option]) {
this.selected[option] = true
} else {
this.selected[option] = !this.selected[option]
}
}
}
}
</script>
<style scoped>
ul {
list-style: none;
}
</style>
ポイント
クリックイベントを拾った時に動くのは下記のコード。なんだか良さげに見えますね。
if (!this.selected[option]) {
this.selected[option] = true
} else {
this.selected[option] = !this.selected[option]
}
結果
- ラベルはクリックできるようになった!が、チェックボックスチェックが入らない...
- チェックボックスクリック時にはチェック状態が変わる!
- あれ?でもプロパティにoptionの値が追加されない...
なぜうまくいかないか
調べてみると、ドキュメントに次のようにありました。
「モダンな JavaScript の制限(そして Object.observe の断念)のため、Vue.js はプロパティの追加または削除を検出できません。Vue.js はインスタンスの初期化中に、getter/setter 変換処理を実行するため、プロパティは、Vue がそれを変換しそしてそれをリアクティブにするために、data オブジェクトに存在しなければなりません」
「ええー?詰んだ...」と思って読み進めると、
Vue はすでに作成されたインスタンスに対して動的に新しいルートレベルのリアクティブなプロパティを追加することはできません。しかしながら Vue.set(object, key, value) メソッドを使うことで、ネストしたオブジェクトにリアクティブなプロパティを追加することができます
とありました。動的にリアクティブなオブジェクトを追加するにはVue.set
を使う必要があります。
動的にリアクティブなプロパティを追加する(良い例)
ツンデレドキュメントから救いを得たのでVue.set
を使ってみましょう。
this.selected[option] = true
の部分を書き換えてみます。単一ファイルコンポーネントの場合、this.$set
で呼び出せます。
<template>
<ul>
<li>
<input
type="checkbox"
v-model="selected.option1"
v-on:click="select('option1')" />
<label
v-on:click="select('option1')">
選択肢1
</label>
</li>
<li>
<input
type="checkbox"
v-model="selected.option2"
v-on:click="select('option2')" />
<label
v-on:click="select('option2')">
選択肢2
</label>
</li>
<li>
<input
type="checkbox"
v-model="selected.option3"
v-on:click="select('option3')" />
<label
v-on:click="select('option3')">
選択肢3
</label>
</li>
</ul>
</template>
<script>
export default {
data () {
return {
selected: {}
}
},
methods: {
select (option) {
console.log(option)
if (!this.selected[option]) {
this.$set(this.selected, option, true)
} else {
this.selected[option] = !this.selected[option]
}
}
}
}
</script>
<style scoped>
ul {
list-style: none;
}
</style>
結果
うまくいきました〜!
参考
チェックボックスなどのバインディングについては良い感じのドキュメントがありました。
label
タグにfor
属性を追加すれば今回みたいなややこしいことしなくてよかったのか〜。
まぁでも考え方はどっかで役に立つはず...!