6.

Vue.js v-model 完全ガイド(双方向データバインディングと修飾子)

編集
この記事の要点
  • v-modelvalue 属性と input イベントを束ねた 双方向データバインディングのシンタックスシュガー
  • 基本: <input v-model="message"> + data() { return { message: "" } }
  • 修飾子: .lazy(change イベント)/ .trim(前後空白除去)/ .number(数値変換)
  • checkbox / radio / select でも使えるが、初期値や bind 値の型に注意
  • カスタムコンポーネントでは Vue 3 は modelValue + update:modelValue、Vue 2 は value + input

v-model とは

Vue.js の v-model は、フォーム入力要素と Vue インスタンスのデータプロパティを双方向にバインドするディレクティブです。ユーザーが入力を変えれば data が、data を更新すれば入力欄が、それぞれ自動更新されます。

これは内部的には 「value 属性のバインド」+「input イベントのリスナー」のシンタックスシュガーです:

<!-- v-model -->
<input v-model="message">

<!-- 上は以下と等価 -->
<input :value="message" @input="message = $event.target.value">

基本の使い方

<div id="app">
  <input v-model="message" placeholder="入力してください">
  <p>入力中: {{ message }}</p>
</div>

<script>
const { createApp } = Vue;
createApp({
  data() {
    return {
      message: ''
    }
  }
}).mount('#app');
</script>

入力するたびに {{ message }} が即座に反映されます。

修飾子 (Modifiers)

修飾子動作用途
.lazyinput ではなく change イベントで同期確定時のみ更新したい(IME 対応)
.number入力値を Number にキャスト数値入力欄
.trim前後の空白を除去ユーザー名・メール等
<!-- フォーカスが外れたタイミングで更新 -->
<input v-model.lazy="searchQuery">

<!-- 数値入力(文字列ではなく Number として data に格納)-->
<input type="number" v-model.number="age">

<!-- 前後の空白を自動除去 -->
<input v-model.trim="username">

<!-- 組み合わせも可能 -->
<input v-model.lazy.trim="comment">

checkbox / radio / select との連携

<!-- 単一チェックボックス (boolean) -->
<input type="checkbox" v-model="agreed">
<!-- data: { agreed: false } -->

<!-- 複数チェックボックス (配列) -->
<input type="checkbox" value="apple"  v-model="fruits">
<input type="checkbox" value="banana" v-model="fruits">
<input type="checkbox" value="orange" v-model="fruits">
<!-- data: { fruits: [] } → 選択された value が配列に -->

<!-- ラジオボタン -->
<input type="radio" value="male"   v-model="gender">
<input type="radio" value="female" v-model="gender">
<input type="radio" value="other"  v-model="gender">

<!-- セレクトボックス -->
<select v-model="selectedCity">
  <option disabled value="">選択してください</option>
  <option>Tokyo</option>
  <option>Osaka</option>
  <option>Nagoya</option>
</select>

<!-- 複数選択 select (Ctrl/Shift で複数) -->
<select v-model="selectedTags" multiple>
  <option>html</option>
  <option>css</option>
  <option>js</option>
</select>

computed と組み合わせる

v-model だけで完結しない加工処理は computedwatch と組み合わせます:

createApp({
  data() {
    return { firstName: '', lastName: '' }
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`.trim();
    }
  },
  watch: {
    fullName(newVal) {
      console.log('変更:', newVal);
    }
  }
}).mount('#app');

computed プロパティに v-model する(setter)

computed は通常読み取り専用ですが、getter/setter を定義すれば v-model にも使えます:

computed: {
  fullName: {
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
    set(value) {
      const parts = value.split(' ');
      this.firstName = parts[0];
      this.lastName  = parts[1] ?? '';
    }
  }
}

カスタムコンポーネントでの v-model

Vue 3 (modelValue / update:modelValue)

// MyInput.vue
<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  >
</template>

<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

// 親側
<MyInput v-model="username" />

Vue 3: 複数の v-model

<UserForm v-model:name="username" v-model:age="userage" />

<!-- 子側 props: ['name', 'age'], emits: ['update:name', 'update:age'] -->

Vue 2 (value / input)

// MyInput.vue (Vue 2)
<template>
  <input :value="value" @input="$emit('input', $event.target.value)">
</template>

<script>
export default {
  props: ['value']
}
</script>

IME (日本語入力) で困る話

デフォルトの v-modelinput イベントで同期するため、IME 変換中の未確定文字も data に反映されてしまいます。検索ボックスなどで「タイピング中に都度 API を叩いて重い」問題の原因です:

<!-- IME 確定時のみ更新したい -->
<input v-model.lazy="searchQuery">

<!-- またはデバウンス -->
<input :value="searchQuery"
       @input="onInput($event)"
       @compositionend="onInput($event)">

<script>
methods: {
  onInput: debounce(function(e) {
    this.searchQuery = e.target.value;
  }, 300)
}
</script>

よくあるトラブル

症状原因対処
入力が反映されないdata() に message を定義していない必ず data に初期値を宣言
v-model.number で空入力時 NaN空文字を Number キャストv-model だけにして手動 parseInt
v-model.lazy で送信ボタンを押しても古い値change が発火していない送信前に明示的に blur
カスタムコンポで v-model 効かないprops/emit 名が違うVue 3 は modelValue / update:modelValue

FAQ

Q: v-bind と v-model の違い
A: v-bind は片方向(data → DOM のみ)、v-model は双方向

Q: v-model のままで textarea や contenteditable に使える?
A: textarea は v-model 可。contenteditable は標準サポートなしで、自前で input イベントを拾う必要があります。

Q: ref とどっちを使うべき?
A: Composition API の ref/reactive + v-model はそのまま動きます。Options API の data() と同じ感覚で使えます。

📸 参考画像

※ 旧バージョンから引き継いだ参考画像です。手順・図解の補助としてご覧ください。

参考画像

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. インストール(ファイルのダウンロード)
  2. npmを使用したプロジェクトの作成
  3. for 繰り返し処理
  4. ifの条件分岐とtemplateを用いたグループ化
  5. クリック時のイベント処理(on:click)
  6. modelとdata フォーム入力値とDOMへの即時反映
  7. computed(算出プロパティ)と使い方とdataとの違い
  8. ライフサイクルフック(created / mounted / updated / destroyedの使い方)
  9. $nextTickの使い方(ライフサイクルフック)
  10. メソッドの定義方法
  11. エラー一覧
  12. ルーティング設定
  13. aリンクの貼り方と動的URLの作成
  14. Mixinを利用した共通処理の記述方法
  15. v-bindによるデータ連携
  16. ヘッダー/フッターの共通コンポーネント
  17. ナビゲーションの現在ページをハイライトする方法
  18. 画面サイズの取得方法

最近更新/作成されたページ