はじめに
v-modelはVue.jsを使ってフォームを構築する際によく使う機能です。
しかし、意外ときちんと使えていない場面を見かけることが多いので仕様と間違えやすいところを簡単に整理します。
v-modelの動作
公式のリファレンスにある通り、v-modelはv-onとv-bindをまとめて一行で書くためのシュガーシンタックスです
つまり以下の二行は同じ動作をします
<input v-model="searchText">
<input :value="searchText" @input="searchText = $event.target.value">
※上記はテキストボックス <input type="text">
での例です。
公式リファレンスにあるように、チェックボックス、ラジオボタン、セレクトボックスの場合はプロパティが異なります。
表にまとめると、次のようになります。
v-model
を使ったときの展開のされ方
フォーム項目の種類 | v-bind: |
v-on: |
---|---|---|
テキストボックス、テキストエリア | value |
input |
チェックボックス、ラジオボタン | checked |
change |
セレクトボックス | value |
change |
カスタムコンポーネントでのv-model
カスタムコンポーネントに対してv-modelを利用した場合、デフォルトではvalue
というpropsとinput
のイベントが使われます。
この値はカスタムコンポーネント側の定義で変更することが出来ます
以下は公式サンプルの引用です。
Vue.component('my-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
// これによって、 `value` プロパティを別の目的で利用することを許可します
value: String,
// `value` の代わりとなるプロパティとして `checked` を使います
checked: {
type: Number,
default: 0
}
},
// ...
})
ありがちな間違い
v-modelと@change
を両方書く
先述の通りv-modelはイベントハンドリングそのものを兼ねているため、v-modelと@changeのように二重に書くとおかしなことになります。
例えば以下のようなコードです。
<input v-model="searchText" @change="(value) => searchText = value">
v-modelはそれ自体がchangeイベントのハンドリングも持っているため、change
に対するイベントが重複してしまいます。
もしchange
イベントで単なる代入以上の処理をしたい場合にはv−modelの利用自体を諦めて素直にv-bindとv-onを別々に書くか、searchTextの算出プロパティを使ってコードを書きます
<input v-model="searchText">
<script>
data(){
return {
innerSearchText: ''
}
},
computed: {
searchText: {
get () {
return this.innerSearchText
},
set (value) {
this.innerSearchText = value
}
}
}
</script>
<input :value="searchText" @change="onChange">
<script>
data(){
return {
searchText: ''
}
}
</script>
v-modelとsetの組み合わせは強力で、Vuexなどと組み合わせた場合にも力を発揮します
propsをそのままv-modelに渡してしまう
v-modelの変数としてpropsをそのまま橋渡しにしたくなる場合はよく出てきますが、これをやってしまうとpropsの値を直接書き換えてしまうため、警告が表示されます
<input v-model="searchText"> # v-modelは searchText = $event.taget.value を実行するため警告が出る。
<script>
props: {
searchText: String
}
</script>
また、これを回避しようとしてpropsの値からdataを定義してしまうのも同じく間違いです
以下のコードはそのサンプルです。
<input v-model="searchTextData">
<script>
props: {
searchText: String
},
data() {
return {
searchTextData: this.$props.searchText
}
}
</script>
このコードは一見正しく動くように見えるのですが、propsの値が初期値に使われた後はdataとpropsの間で値が同期されないため、
親コンポーネントから別の値を送り込んでも反映されなくなります。
それではどうすればいいかというとこれも算出プロパティを使うと回避可能です
<input v-model="innerSearchText">
<script>
props: {
searchText: String
},
computed: {
innerSearchText: {
get () {
return this.$props.searchText
},
set (value) {
this.$emit('change', value)
}
}
}
</script>
つまりこのコンポーネントの中で値を代入するのではなくさらに親のコンポーネントに対して値をイベントとして送り出します。
そして親のコンポーネント側でpropsの値を書き換えてやることでこのコンポーネントを正しく使うことができるようになります。
setterのかわりにwatchを使う手法もありますが、データの流れが見えづらくなるので個人的にあまりオススメしません。
余談
v-modelはサンプルコードではよく出てきますしそれぐらいのコードであれば便利なのですが、
実際のアプリケーションで値の代入だけで済むことは少なく、changeやinputイベントをキャッチしてなんらかの処理を書かないといけない場面が多いのではないかと思います。
そのためv-modelにこだわらないほうが色々とシンプルにかける場面は多いので気をつけたほうがいいかなと感じています。
ただしv-modelにはモディファイヤが用意されていてこれが便利なので(特にnumber)、
適材適所で使っていけると良いかなと思います