【Vue】v-modelでフォームの双方向バインディングを実装する

JavaScript

こんにちは、かつコーチです。

前回は v-bind で属性を動的にバインドする方法を扱いました。

今回は、フォーム入力とデータをつなぐ v-model について解説します。

v-model はVueの中でも特に「魔法っぽく見える」機能ですが、中身を分解すると前回・前々回で学んだ v-bindv-on の組み合わせにすぎません。

その仕組みを理解しながら、実践的な使い方を見ていきましょう。

v-modelとは?

双方向バインディングという考え方

v-model は、入力フォームの値とデータを、双方向に同期させるディレクティブです。

<script setup lang="ts">
import { ref } from 'vue'

const name = ref<string>('')
</script>

<template>
  <input v-model="name" type="text" />
  <p>入力された名前:{{ name }}</p>
</template>

入力欄に文字を打ち込むと、その内容がリアルタイムで name に反映され、<p> タグの表示も同時に更新されます。

「データが変わったら画面が変わる」だけでなく、「画面(入力)が変わったらデータも変わる」という双方向の連動が、v-model の最大の特徴です。

v-modelの中身:v-bindとv-onの組み合わせ

実は v-model は、以下のコードとほぼ同じ意味です。

<script setup lang="ts">
import { ref } from 'vue'

const name = ref<string>('')

const handleInput = (event: Event): void => {
  name.value = (event.target as HTMLInputElement).value
}
</script>

<template>
  <input :value="name" @input="handleInput" />
</template>

:value="name" で現在のデータを表示し、@input で入力のたびにデータを更新する。

この2つを1つのディレクティブにまとめて省略したものが v-model です。

前回・前々回で学んだ知識が、実はこの便利な機能の土台になっていたわけです。

基本の書き方

テキスト入力・テキストエリア

一番基本的な使い方は、inputtextarea でのテキスト入力です。

<script setup lang="ts">
import { ref } from 'vue'

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

<template>
  <textarea v-model="message" placeholder="メッセージを入力"></textarea>
  <p>文字数:{{ message.length }}</p>
</template>

message の値を使って文字数をリアルタイムに表示するような処理も、computed を組み合わせれば簡単に実現できます。

チェックボックス・ラジオボタン

チェックボックスは、ref<boolean> と組み合わせるのが基本形です。

<script setup lang="ts">
import { ref } from 'vue'

const isAgreed = ref<boolean>(false)
</script>

<template>
  <label>
    <input v-model="isAgreed" type="checkbox" />
    利用規約に同意する
  </label>
  <button :disabled="!isAgreed">送信</button>
</template>

チェックボックスが複数ある場合は、配列と組み合わせることで「選択された項目一覧」を取得できます。

<script setup lang="ts">
import { ref } from 'vue'

const selectedFruits = ref<string[]>([])
</script>

<template>
  <label><input v-model="selectedFruits" type="checkbox" value="りんご" />りんご</label>
  <label><input v-model="selectedFruits" type="checkbox" value="みかん" />みかん</label>
  <p>選択中:{{ selectedFruits.join('、') }}</p>
</template>

同じ v-model="selectedFruits" を複数のチェックボックスに指定するだけで、チェックされた value が自動的に配列にまとめられます。

セレクトボックス

select タグでも同様に、選択された値がそのまま反映されます。

<script setup lang="ts">
import { ref } from 'vue'

const prefecture = ref<string>('')
</script>

<template>
  <select v-model="prefecture">
    <option disabled value="">選択してください</option>
    <option value="tokyo">東京都</option>
    <option value="osaka">大阪府</option>
  </select>
</template>

最初の <option disabled value=""> は、初期状態で何も選ばれていないことを表す定番のパターンです。

よくあるつまずきポイント・エラー対処

数値入力なのに文字列として扱ってしまう

私が実際につまずいたのが、数値入力フォームでの型の扱いでした。

❌ Before:type=”number”でも値は文字列になる

<script setup lang="ts">
import { ref } from 'vue'

const age = ref<string>('') // 本当は数値として扱いたい

const isAdult = (): boolean => {
  // 文字列同士の比較になってしまい、意図通りに動かないことがある
  return age.value >= '20'
}
</script>

<template>
  <input v-model="age" type="number" />
</template>

type="number" を指定していても、v-model で受け取る値はデフォルトでは文字列です。

私はこれを知らずに数値として比較する処理を書いてしまい、"9""20" より大きいと判定されるような、文字列比較特有のバグに悩まされたことがあります(文字列としては "9" の方が "20" より辞書順で後ろに来るため)。

✅ After:.number修飾子で自動的に数値へ変換する

<script setup lang="ts">
import { ref } from 'vue'

const age = ref<number | null>(null)

const isAdult = (): boolean => {
  return (age.value ?? 0) >= 20
}
</script>

<template>
  <input v-model.number="age" type="number" />
</template>

v-model.number のように修飾子を付けると、入力された値を自動的に数値へ変換してから ref に格納してくれます。

これで age.value を数値として安心して扱えるようになり、比較処理も意図通りに動きます。

空白の入力をそのまま受け取ってしまう

もう一つ、検索フォームなどでよくあるのが前後の空白の扱いです。

<script setup lang="ts">
import { ref } from 'vue'

const keyword = ref<string>('')
</script>

<template>
  <!-- "  りんご  " のように前後に空白が入っても、そのまま検索されてしまう -->
  <input v-model.trim="keyword" type="text" />
</template>

.trim 修飾子を付けると、入力値の前後の空白を自動的に取り除いてから ref に格納してくれます。

検索キーワードやメールアドレスの入力欄など、余計な空白を混入させたくない場面では積極的に使っておくと安心です。

修飾子効果
.number入力値を数値に変換する
.trim入力値の前後の空白を取り除く
.lazyinputイベントではなくchangeイベント(フォーカスが外れた時など)で同期する

応用・一歩先の使い方

自作コンポーネントでv-modelを使う

v-modelinput タグだけでなく、自分で作ったコンポーネントにも適用できます。

<!-- CustomInput.vue -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()

const handleInput = (event: Event): void => {
  emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>

<template>
  <input :value="modelValue" @input="handleInput" />
</template>
<!-- 親コンポーネント -->
<script setup lang="ts">
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const keyword = ref<string>('')
</script>

<template>
  <CustomInput v-model="keyword" />
</template>

modelValue というプロパティ名と update:modelValue というイベント名の組み合わせが、v-model の内部的な約束事です。

この仕組みを理解しておくと、独自のフォーム部品を作るときにも迷わなくなります。

まとめ

この記事のポイント

  • v-modelv-bindv-on を組み合わせた「双方向バインディング」の省略記法
  • テキスト入力・チェックボックス・セレクトボックスなど、フォーム要素と幅広く組み合わせられる
  • 数値入力には .number、空白除去には .trim などの修飾子を活用する
  • 自作コンポーネントにも modelValue / update:modelValue の約束事で v-model を適用できる

次に読むべき記事

次回は、リストの繰り返し表示と条件分岐を扱う v-forv-if の書き方を解説します。

→ 次の記事:v-for・v-ifでリスト表示と条件分岐を書く

タイトルとURLをコピーしました