【Vue.js】v-modelの仕組みを完全理解|双方向バインディングの裏側とカスタム実装

【Vue.js】v-modelの仕組みを完全理解|双方向バインディングの裏側とカスタム実装 Vue.js

v-modelは、フォームとデータを双方向に結びつける、基本のディレクティブです。便利なぶん、中で何が起きているかを知らないと、日本語入力で更新されない、自作コンポーネントで値が親に伝わらない、といった場面で困ります。この記事では、Vue 3を前提に、v-modelの仕組みと、コンポーネントへの実装方法を整理します。フォーム部品ごとの使い方は双方向データバインディングでフォームの入力値を反映させる方法で解説しています。

スポンサーリンク

ネイティブ要素でのv-model

v-modelは、手書きの:valueと@inputを、短く書くための構文です。公式でも、この2つの手書きの書き方と並べて説明されています。

Vue:v-modelと手書きの書き方
<script setup>
import { ref } from 'vue'

const text = ref('')
</script>

<template>
  <!-- v-model を使った書き方 -->
  <input v-model="text">

  <!-- ほぼ同じ意味の、手書きの書き方 -->
  <input :value="text" @input="event => text = event.target.value">
</template>

日本語入力(IME)では、完全には同じではない

ただし、両者は完全に同じ動きではありません。公式は、IMEを使う言語(日本語、中国語、韓国語など)では、v-modelは変換中に更新されないと説明しています。変換中も反応したい場合は、v-modelを使わず、:valueと@inputで自前で書く、というのが公式の案内です。つまり「省略形」は、普段の理解としては正しくても、変換中の挙動は別物と考えてください。

要素ごとのプロパティとイベント

v-modelは、使う要素によって、結びつけるDOMのプロパティとイベントの組み合わせを切り替えます。

  • テキスト系の<input>と<textarea>:valueプロパティとinputイベント
  • チェックボックスとラジオボタン:checkedプロパティとchangeイベント
  • <select>:valueプロパティとchangeイベント

修飾子.lazy、.number、.trimの使い方は、3000の記事をご覧ください。

コンポーネントでのv-model

自作のコンポーネントにv-modelを使うと、Vueは次のように展開します。子コンポーネントは、modelValueというpropを受け取り、値を変えたいときにupdate:modelValueイベントを発行する、という約束になります。

Vue:コンポーネントのv-modelの展開
<!-- この書き方は -->
<CustomInput v-model="searchText" />

<!-- 次のように展開される -->
<CustomInput
  :model-value="searchText"
  @update:model-value="newValue => searchText = newValue"
/>

defineModel()で実装する(Vue 3.4以降)

Vue 3.4以降は、defineModel()を使うのが推奨です。propsとイベントの定義を自分で書かずに済みます。返ってくるrefは、親のv-modelの値と同期しており、子で変更すると親の値も更新されます。ネイティブの<input>には、そのままv-modelで渡せます。

Vue:defineModelによる子コンポーネント
<!-- CustomInput.vue(Vue 3.4 以降) -->
<script setup>
const model = defineModel()
</script>

<template>
  <!-- 返ってきた ref を、ネイティブ入力の v-model にそのまま渡せる -->
  <input v-model="model">
</template>
Vue:親コンポーネントからの使い方
<!-- 親コンポーネント -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const myText = ref('')
</script>

<template>
  <CustomInput v-model="myText" />
  <p>{{ myText }}</p>
</template>

3.4より前の書き方:propsとemits

3.4より前のバージョンや、既存コードでは、propsとイベントを自分で書きます。update:modelValueはemitsに宣言してください。

Vue:propsとemitsによる子コンポーネント
<!-- CustomInput.vue(3.4 より前の書き方) -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  >
</template>
Options API:propsとemitsの宣言
// Options API の場合
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}

必須にする・既定値を持たせる

Vue:defineModelのオプション
// 必須にする
const model = defineModel({ required: true })

// 既定値を持たせる
const model = defineModel({ default: 0 })

注意点として、defaultを指定して、親が値を渡さない場合は、親子の値がずれることがあります。親が必ず値を持つ設計なら、requiredを使うのが安全です。

v-modelの引数で複数の値をバインドする

v-model:first-nameのように、v-modelに引数を付けると、1つのコンポーネントに複数のv-modelを使えます。これを「エイリアス」と呼ぶ記事もありますが、公式の名称は「v-modelの引数」です。子側はdefineModel('firstName')のように、引数の名前を渡します。

Vue:親でv-modelに引数を付ける
<!-- 親:v-model に引数を付けて、複数をバインド -->
<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>
Vue:子で引数の名前を指定する
<!-- UserName.vue -->
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
  <input type="text" v-model="firstName">
  <input type="text" v-model="lastName">
</template>

カスタム修飾子を作る

自作のコンポーネントには、自分で決めた修飾子も付けられます。defineModel()の第2戻り値modifiersに、親が付けた修飾子が入ります。値が更新されるときに加工するsetと組み合わせます。

Vue:親でカスタム修飾子を付ける
<!-- 親:カスタム修飾子 .capitalize を付ける -->
<MyInput v-model.capitalize="myText" />
Vue:子でカスタム修飾子を処理する
<!-- MyInput.vue(3.4 以降) -->
<script setup>
// 第2戻り値の modifiers に、親が付けた修飾子が入る
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input type="text" v-model="model">
</template>

3.4より前は、modelModifiersというpropで修飾子を受け取り、イベントを発行する前に値を加工します。

Vue 2からの変更点

Vue 2のコードを流用すると、値が親に伝わらないことがあります。主な変更は次のとおりです。

  • コンポーネントのv-modelが使うprop名とイベント名が、valueとinputから、modelValueとupdate:modelValueに変わった
  • v-bindの.sync修飾子と、コンポーネントのmodelオプションは廃止され、v-modelの引数に置き換えられた
  • 1つのコンポーネントに、複数のv-modelを使えるようになった

関連記事

よくある質問(FAQ)

Qv-modelは、:valueと@inputを書くのと同じですか?
Aほぼ同じ意味ですが、IMEで変換している間は違います。v-modelは変換中には更新されず、確定したときに反映されます。変換中も反応したいなら、:valueと@inputで自前で書いてください。
Q自作コンポーネントにv-modelを付けたのに、親に値が伝わりません。
AVue 2のvalueとinputのままになっている可能性があります。Vue 3では、modelValueを受け取り、update:modelValueを発行する必要があります。3.4以降ならdefineModel()が簡単です。
Q1つのコンポーネントに、複数のv-modelを使えますか?
A使えます。v-model:first-nameのように引数を付けて、子でdefineModel('firstName')のように、同じ名前を指定します。
QdefineModel()のdefaultを指定すると、値がずれることはありますか?
Aあります。親が値を渡さないのに、子にdefaultがあると、親子の値がずれる場合があります。親が必ず値を持つ設計なら、required: trueを使うほうが安全です。

まとめ

v-modelは、ネイティブ要素ではvalueやcheckedとイベントの組み合わせ、コンポーネントではmodelValueとupdate:modelValueのペアで成り立っています。日本語では、変換中に更新されないという違いに気をつけ、自作のコンポーネントではdefineModel()を使うと、短く、間違いなく書けます。Vue 2のコードは、prop名とイベント名の変更に注意して移してください。