この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:8
ページ更新者:guest
更新日時:2026-06-11 07:07:02

タイトル: modelとdata フォーム入力値とDOMへの即時反映
SEOタイトル: Vue.js v-model 完全ガイド(双方向データバインディングと修飾子)

この本文には実行可能なタグ等が含まれるため、安全のためHTMLソースとして表示しています。本文データは変更していません。

<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;background:#fffbea;border:1px solid #f4c430;margin:1em 0;width:100%"><tr><td style="background:#f4c430;color:#000;font-weight:bold;padding:8px">この記事の要点</td></tr><tr><td style="padding:10px"><ul><li><strong>v-model</strong> は <code>value</code> 属性と <code>input</code> イベントを束ねた <strong>双方向データバインディング</strong>のシンタックスシュガー</li><li><strong>基本</strong>: <code><input v-model="message"></code> + <code>data() { return { message: "" } }</code></li><li><strong>修飾子</strong>: <code>.lazy</code>(change イベント)/ <code>.trim</code>(前後空白除去)/ <code>.number</code>(数値変換)</li><li><strong>checkbox / radio / select</strong> でも使えるが、初期値や bind 値の型に注意</li><li>カスタムコンポーネントでは Vue 3 は <code>modelValue</code> + <code>update:modelValue</code>、Vue 2 は <code>value</code> + <code>input</code></li></ul></td></tr></table>
<h2>v-model とは</h2>

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

<p>これは内部的には <strong>「value 属性のバインド」+「input イベントのリスナー」</strong>のシンタックスシュガーです:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><!-- v-model -->
<input v-model="message">

<!-- 上は以下と等価 -->
<input :value="message" @input="message = $event.target.value"></code></pre>
<h2>基本の使い方</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><div id="app">
  <input v-model="message" placeholder="入力してください">
  <p>入力中: {{ message }}</p>
</div>

<script>
const { createApp } = Vue;
createApp({
  data() {
    return {
      message: &apos;&apos;
    }
  }
}).mount(&apos;#app&apos;);
</script></code></pre>
<p>入力するたびに <code>{{ message }}</code> が即座に反映されます。</p>

<h2>修飾子 (Modifiers)</h2>

<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#e3f2fd"><th>修飾子</th><th>動作</th><th>用途</th></tr></thead>
<tbody>
<tr><td><code>.lazy</code></td><td>input ではなく change イベントで同期</td><td>確定時のみ更新したい(IME 対応)</td></tr>
<tr><td><code>.number</code></td><td>入力値を Number にキャスト</td><td>数値入力欄</td></tr>
<tr><td><code>.trim</code></td><td>前後の空白を除去</td><td>ユーザー名・メール等</td></tr>
</tbody>
</table>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><!-- フォーカスが外れたタイミングで更新 -->
<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"></code></pre>
<h2>checkbox / radio / select との連携</h2>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><!-- 単一チェックボックス (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></code></pre>
<h2>computed と組み合わせる</h2>

<p>v-model だけで完結しない加工処理は <strong>computed</strong> や <strong>watch</strong> と組み合わせます:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">createApp({
  data() {
    return { firstName: &apos;&apos;, lastName: &apos;&apos; }
  },
  computed: {
    fullName() {
      return `${this.firstName} ${this.lastName}`.trim();
    }
  },
  watch: {
    fullName(newVal) {
      console.log(&apos;変更:&apos;, newVal);
    }
  }
}).mount(&apos;#app&apos;);</code></pre>
<h2>computed プロパティに v-model する(setter)</h2>

<p>computed は通常読み取り専用ですが、getter/setter を定義すれば v-model にも使えます:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">computed: {
  fullName: {
    get() {
      return `${this.firstName} ${this.lastName}`;
    },
    set(value) {
      const parts = value.split(&apos; &apos;);
      this.firstName = parts[0];
      this.lastName  = parts[1] ?? &apos;&apos;;
    }
  }
}</code></pre>
<h2>カスタムコンポーネントでの v-model</h2>

<h3>Vue 3 (modelValue / update:modelValue)</h3>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// MyInput.vue
<template>
  <input
    :value="modelValue"
    @input="$emit(&apos;update:modelValue&apos;, $event.target.value)"
  >
</template>

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

// 親側
<MyInput v-model="username" /></code></pre>
<h3>Vue 3: 複数の v-model</h3>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><UserForm v-model:name="username" v-model:age="userage" />

<!-- 子側 props: [&apos;name&apos;, &apos;age&apos;], emits: [&apos;update:name&apos;, &apos;update:age&apos;] --></code></pre>
<h3>Vue 2 (value / input)</h3>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-javascript">// MyInput.vue (Vue 2)
<template>
  <input :value="value" @input="$emit(&apos;input&apos;, $event.target.value)">
</template>

<script>
export default {
  props: [&apos;value&apos;]
}
</script></code></pre>
<h2>IME (日本語入力) で困る話</h2>

<p>デフォルトの <code>v-model</code> は <code>input</code> イベントで同期するため、<strong>IME 変換中の未確定文字も data に反映</strong>されてしまいます。検索ボックスなどで「タイピング中に都度 API を叩いて重い」問題の原因です:</p>
<pre style="background:#1e293b;color:#e2e8f0;padding:1em;border-radius:6px;overflow-x:auto;line-height:1.5"><code class="language-html"><!-- 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></code></pre>
<h2>よくあるトラブル</h2>

<table border="1" cellpadding="6" cellspacing="0" style="border-collapse:collapse;width:100%">
<thead><tr style="background:#fff3e0"><th>症状</th><th>原因</th><th>対処</th></tr></thead>
<tbody>
<tr><td>入力が反映されない</td><td>data() に message を定義していない</td><td>必ず data に初期値を宣言</td></tr>
<tr><td>v-model.number で空入力時 NaN</td><td>空文字を Number キャスト</td><td>v-model だけにして手動 parseInt</td></tr>
<tr><td>v-model.lazy で送信ボタンを押しても古い値</td><td>change が発火していない</td><td>送信前に明示的に blur</td></tr>
<tr><td>カスタムコンポで v-model 効かない</td><td>props/emit 名が違う</td><td>Vue 3 は modelValue / update:modelValue</td></tr>
</tbody>
</table>

<h2>FAQ</h2>

<p><strong>Q: v-bind と v-model の違い</strong><br />
A: v-bind は<strong>片方向</strong>(data → DOM のみ)、v-model は<strong>双方向</strong>。</p>

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

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

<!-- @RESTORED_IMG_v1 -->
<h2>📸 参考画像</h2>
<p style="color:#666;font-size:0.85em;margin-bottom:1em">※ 旧バージョンから引き継いだ参考画像です。手順・図解の補助としてご覧ください。</p>
<p style="text-align:center;margin:1em 0"><img alt="参考画像" src="https://jpn.itlibra.com/uploadImage/20190629223504_7065356.png" style="max-width:640px;width:100%;height:auto;border:1px solid #ccc;border-radius:4px" /></p>