【Vue.js】双方向データバインディングでフォームの入力値をリアルタイムに反映させる方法

【Vue.js】双方向データバインディングでフォームの入力値をリアルタイムに反映させる方法 Vue.js

フォームの入力値とデータを同期させるのが、v-modelです。入力するとデータが更新され、データを書き換えると入力欄の表示も変わります。この記事では、Vue 3(<script setup>)を前提に、フォーム部品ごとの使い方と、日本語入力で戸惑いやすい点を動くコードで整理します。

スポンサーリンク

テキスト入力のバインディング

入力欄にv-modelを付けるだけで、入力した文字が変数に反映されます。<textarea>では{{ }}が使えないため、必ずv-modelを使います。

Vue:テキスト入力とtextarea
<script setup>
import { ref } from 'vue'

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

<template>
  <input v-model="message">
  <p>{{ message }}</p>

  <!-- textarea では {{ }} が使えない。v-model を使う -->
  <textarea v-model="message"></textarea>
</template>

日本語入力(IME)では変換中に更新されない

日本語のようにIMEで変換する言語では、v-modelは変換中には更新されません。確定したときに初めてデータに反映されます。入力中の文字をそのままリアルタイムに表示したい場合は、v-modelの代わりに:valueと@inputを自分で書きます。

Vue:変換中も反映させる
<script setup>
import { ref } from 'vue'

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

<template>
  <!-- v-model の代わりに :value と @input を自分で書く -->
  <input :value="text" @input="text = $event.target.value">
  <p>{{ text }}</p>
</template>

チェックボックスのバインディング

チェックボックスは、使い方で受け取る値が変わります。1つだけなら真偽値、同じ変数に複数を結びつけると配列になります。true-valueとfalse-valueを指定すれば、真偽値の代わりに任意の値も使えます。

Vue:チェックボックス
<script setup>
import { ref } from 'vue'

const agreed = ref(false)        // 1つのチェックボックス → 真偽値
const names = ref([])            // 複数のチェックボックス → 配列
const toggle = ref('no')         // true-value / false-value
</script>

<template>
  <label><input type="checkbox" v-model="agreed"> 同意する</label>
  <p>{{ agreed }}</p>

  <label><input type="checkbox" value="Jack" v-model="names"> Jack</label>
  <label><input type="checkbox" value="John" v-model="names"> John</label>
  <p>{{ names }}</p>

  <input type="checkbox" v-model="toggle" true-value="yes" false-value="no">
  <p>{{ toggle }}</p>
</template>

ラジオボタンのバインディング

同じ変数に結びつけたラジオボタンのうち、選択されたもののvalueが変数に入ります。

Vue:ラジオボタン
<script setup>
import { ref } from 'vue'

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

<template>
  <label><input type="radio" value="One" v-model="picked"> One</label>
  <label><input type="radio" value="Two" v-model="picked"> Two</label>
  <p>{{ picked }}</p>
</template>

セレクトボックスのバインディング

単一選択は文字列、multipleを付けると配列になります。

Vue:セレクトボックス
<script setup>
import { ref } from 'vue'

const selected = ref('')       // 単一選択 → 文字列
const multi = ref([])          // multiple → 配列
</script>

<template>
  <select v-model="selected">
    <option disabled value="">選択してください</option>
    <option>Option 1</option>
    <option>Option 2</option>
  </select>
  <p>{{ selected }}</p>

  <select v-model="multi" multiple>
    <option>Option 1</option>
    <option>Option 2</option>
    <option>Option 3</option>
  </select>
  <p>{{ multi }}</p>
</template>

単一選択の先頭に、disabledで値が空のオプションを置いています。v-modelの初期値がどの選択肢にも一致しないと、セレクトボックスは未選択の状態で表示されます。この状態のiOSでは、先頭の項目を選べなくなります(changeイベントが発火しないため)。公式も、このオプションを置くことを推奨しています。

修飾子で入力値を整える

v-modelには3つの修飾子があります。

Vue:v-modelの修飾子
<!-- change イベントで同期(フォーカスを外したときなど) -->
<input v-model.lazy="msg">

<!-- 数値に変換(parseFloat できなければ元の文字列のまま) -->
<input v-model.number="age">

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

初期値はJavaScript側で宣言する

v-modelは、フォーム要素に書いたvalue、checked、selectedの初期属性を無視します。常にJavaScript側の状態が正で、初期値はそちらで宣言する必要があります。

Vue:HTMLのcheckedは効かない
<!-- NG:HTML側の checked は無視される。常にJavaScript側の状態が正 -->
<input type="checkbox" v-model="agreed" checked>

<!-- OK:初期値はJavaScript側で宣言する -->
<script setup>
import { ref } from 'vue'
const agreed = ref(true)
</script>

カスタムコンポーネントでv-modelを使う

自作の入力コンポーネントにもv-modelが使えます。Vue 3では、子コンポーネントが受け取るpropはmodelValue、通知するイベントはupdate:modelValueです。Vue 2のvalueとinputから名前が変わっています。Vue 2のコードをそのまま使うと、値が親に伝わりません。

Vue 3.4以降は、defineModel()を使うと、propとイベントの定義を省略できます。

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

<template>
  <input v-model="model">
</template>
Vue:子コンポーネント(3.4より前の書き方)
<!-- CustomInput.vue(3.4より前の書き方) -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  >
</template>
Vue:親コンポーネント
<!-- 親コンポーネント -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

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

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

v-modelの内部の仕組みやカスタム実装はv-modelの仕組みを完全理解で詳しく解説しています。

関連記事

よくある質問(FAQ)

Q日本語を入力中に、v-modelの表示が更新されないのはなぜですか?
AIMEで変換している間は、v-modelが更新されない仕様のためです。確定すると反映されます。変換中も反映したいときは、:valueと@inputで自分で書いてください。
Qv-model.numberを付けたのに文字列のままになるのはなぜですか?
A.numberはparseFloat()で変換します。変換できない値(空欄や文字だけの入力)は、元の文字列のままです。
QHTMLにcheckedを書いても、チェックが付かないのはなぜですか?
Av-modelは初期のchecked属性を無視するためです。JavaScript側で、ref(true)のように初期値を宣言してください。
Q自作コンポーネントにv-modelを使ったら、値が親に伝わりません。
AVue 2のコードのままの可能性があります。Vue 3では、子コンポーネントがmodelValueを受け取り、update:modelValueを発行する必要があります。3.4以降ならdefineModel()が簡単です。

まとめ

v-modelは、フォーム部品に合わせて、文字列、真偽値、配列を自動で使い分けてくれます。日本語サイトでは、IMEの変換中に更新されないことを押さえておくと、リアルタイム表示でつまずきません。初期値はJavaScript側で宣言し、コンポーネントではmodelValueとupdate:modelValueを使ってください。