タイトル: 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: ''
}
}
}).mount('#app');
</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: '', lastName: '' }
},
computed: {
fullName() {
return `${this.firstName} ${this.lastName}`.trim();
}
},
watch: {
fullName(newVal) {
console.log('変更:', newVal);
}
}
}).mount('#app');</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(' ');
this.firstName = parts[0];
this.lastName = parts[1] ?? '';
}
}
}</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('update:modelValue', $event.target.value)"
>
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
</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: ['name', 'age'], emits: ['update:name', 'update:age'] --></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('input', $event.target.value)">
</template>
<script>
export default {
props: ['value']
}
</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>