Form Input Bindings - v-model とフォーム状態同期
ロードマップ: Vue.js学習ロードマップ
公式ドキュメント: https://vuejs.org/guide/essentials/forms.html
What(何についてか)
Vue の v-model を使い、フォーム要素と JavaScript 側状態を同期する方法を扱う。
<input>, <textarea>, <checkbox>, <radio>, <select> での基本挙動、値の型、v-bind を使った value binding、v-model modifier、コンポーネントへの拡張可能性を整理する。
Why(なぜ必要か)
フロントエンドでは、フォーム入力をアプリケーション状態へ正確に反映することが重要である。
値表示と入力イベント処理を毎回手書きすると冗長になりやすいが、v-model を使うことで、要素ごとに適切な DOM property と event の組を Vue が吸収してくれる。
これにより、フォーム状態の扱いを簡潔にしつつ、値の型や同期タイミングを必要に応じて調整できる。
How(どう動くか)
1. v-model は value binding と event handling の糖衣構文である
<input
:value="text"
@input="event => text = event.target.value">上記は v-model により次のように簡潔に書ける。
<input v-model="text">ただし v-model は単一の固定ルールではない。
要素種別に応じて、内部的に異なる DOM property と event の組み合わせへ展開される。
- text input / textarea:
value+input - checkbox / radio:
checked+change - select:
value+change
2. source of truth は HTML 属性ではなく JavaScript 側状態である
value, checked, selected のような HTML の初期属性は v-model 使用時の真実ではない。
現在値は常に Vue 側状態が支配する。
そのため、初期値はテンプレート側属性ではなく、ref や reactive などの状態で定義する。
<script setup>
import { ref } from 'vue'
const text = ref('initial value')
</script>
<template>
<input v-model="text" />
</template>3. text input と textarea は文字列を同期する
Text
<p>Message is: {{ message }}</p>
<input v-model="message" placeholder="edit me" />入力のたびに message が更新され、表示にも反映される。
IME に関する注意
日本語など IME を使う入力では、composition 中に v-model が更新されない。
変換途中の入力内容にも反応したい場合は、自前で :value と @input を扱う必要がある。
Multiline Text
<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea><textarea> でも v-model を使う。
改行表示は white-space: pre-line など CSS 側で調整する。
<textarea>{{ text }}</textarea> のような補間による値設定は使わず、v-model で状態と結びつける。
4. checkbox は boolean または複数選択集合を扱う
単一 checkbox
<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>単一 checkbox は boolean と同期する。
- checked →
true - unchecked →
false
複数 checkbox
<div>Checked names: {{ checkedNames }}</div>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>const checkedNames = ref([])複数 checkbox に同じ v-model を与えると、チェックされた項目の value が配列へ入る。
ここで状態に入るのは id ではなく value である。
5. radio は単一選択値を扱う
<div>Picked: {{ picked }}</div>
<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>
<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>radio では選択された項目の value が picked に入る。
checkbox が複数選択を表現できるのに対し、radio は単一選択の状態に向く。
6. select は単一値または複数値の選択を扱う
Single select
<div>Selected: {{ selected }}</div>
<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>単一 select では、選択中 option の値が状態に入る。
初期値がどの option にも一致しない場合、未選択状態になる。
そのため placeholder として disabled option を先頭に置き、初期値を '' にする設計が実務的である。
<select v-model="selected">
<option disabled value="">Please select one</option>
<option
v-for="option in options"
:key="option.value"
:value="option.value"
>
{{ option.text }}
</option>
</select>Multiple select
<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>multiple を付けた場合、状態は配列になる。
7. v-bind を使うと非文字列値や動的値を扱える
radio, checkbox, select の value は通常文字列だが、v-bind を使うと number, object, 変数値を直接扱える。
Checkbox の true-value / false-value
<input
type="checkbox"
v-model="toggle"
true-value="yes"
false-value="no" />checked / unchecked を true / false 以外の値へ対応させられる。
動的値も可能である。
<input
type="checkbox"
v-model="toggle"
:true-value="dynamicTrueValue"
:false-value="dynamicFalseValue" />Radio の :value
<input type="radio" v-model="pick" :value="first" />
<input type="radio" v-model="pick" :value="second" />radio 選択時に変数値を状態へ入れられる。
Select の :value
<select v-model="selected">
<option :value="{ number: 123 }">123</option>
</select>option 選択時に object など非文字列値を状態へ入れられる。
value="1" と :value="1" は別物
value="1"→ 文字列'1':value="1"→ 数値1
型が異なるため、TypeScript 前提ではこの区別が重要になる。
8. v-model modifier で同期挙動を調整できる
.lazy
<input v-model.lazy="msg" />通常は input イベントごとに同期されるが、.lazy を付けると change ベースで同期する。
入力途中ではなく確定寄りのタイミングで状態更新したい場合に使う。
.number
<input v-model.number="age" />入力を number 化しようとする。
内部的には parseFloat() ベースで、空文字は '' のまま返る。
そのため完全な型保証ではない。
また type="number" では自動適用される。
.trim
<input v-model.trim="msg" />前後空白を自動で削除してから状態へ反映する。
9. v-model はコンポーネントにも拡張できる
v-model は組み込みフォーム要素だけでなく、自作コンポーネントにも適用できる。
標準 input で足りない複雑な UI 部品でも、親コンポーネントからは v-model と同じ書き味で扱える。
詳細は Components ガイドの Usage with v-model で扱う。
フォーム同期の全体フロー
flowchart LR A["ユーザー入力"] --> B["DOM event 発生"] B --> C["v-model が受け取る"] C --> D["Vue 状態を更新"] D --> E["テンプレート再描画"]
実務での判断軸
基本の text input
<input v-model="message" />IME 変換途中も追いたい
v-model だけではなく、自前の :value と @input を使う設計を検討する。
checkbox の単一 ON/OFF
<input type="checkbox" v-model="enabled" />checkbox の複数選択
<input type="checkbox" value="A" v-model="selectedItems" />radio の単一選択
<input type="radio" value="admin" v-model="role" />select の placeholder
<select v-model="selected">
<option disabled value="">Please select one</option>
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.text }}
</option>
</select>number として扱いたい
<input v-model.number="age" />ただし空文字や変換失敗時の扱いは別途考える。
Options API → Composition API 差分(補足)
| 項目 | Options API(旧) | Composition API(新) |
|---|---|---|
| 状態定義 | data() に置く | ref() / reactive() に置く |
| テンプレート同期 | v-model の使い方自体はほぼ同じ | 同じだが <script setup> と直接つながる |
| 型意識 | JavaScript 的に使いがち | TypeScript と併用するため :value の型差分を強く意識する |
| handler 補助 | methods と組み合わせる | 通常関数と event.isComposing などを直接扱う |
Key Concepts
| 用語 | 説明 |
|---|---|
v-model | フォーム要素と状態を同期するディレクティブ |
| source of truth | 現在値の正本。v-model では JavaScript 側状態 |
checked | checkbox / radio の選択状態を表す DOM property |
change event | 値確定時に近いタイミングで起きるイベント |
input event | 入力のたびに起きるイベント |
true-value / false-value | checkbox の checked / unchecked を独自値へ写す Vue 専用属性 |
:value | 動的値や非文字列値をフォーム要素に結びつけるバインド |
.lazy | change ベースへ同期タイミングを遅らせる modifier |
.number | 入力値の数値変換を試みる modifier |
.trim | 前後空白を除去する modifier |
event.isComposing | IME 変換中かどうかを判定するためのプロパティ |