イベントとフォーム入力 — 画面からの入力を状態へ戻す
前の章では状態を画面へ写す方向を扱いました。この章は逆向きで、画面での操作を状態へ戻します。
やることは 2 つです。イベントを受け取ってハンドラを呼ぶことと、入力欄の値を状態と結びつけることです。どちらも素直に書けますが、細部に落とし穴があります。ハンドラに括弧を付けるかどうかでイベントオブジェクトの渡り方が変わり、キー修飾子を連結したときの意味は修飾子の種類によって変わります。
この章で学ぶこと
- ハンドラの 3 つの書き方を、イベントオブジェクトの渡り方から選べる
- イベント修飾子で DOM の定型処理をテンプレート側に寄せられる
- キー修飾子とシステム修飾子で連結の意味が変わることを知って使える
v-modelが入力の種類ごとに何を状態へ入れるかを説明できる
イベントを受ける
v-on を付けるとイベントを受け取れます。省略記法は @ です。
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
count.value += 1
}
const incrementBy = (amount: number) => {
count.value += amount
}
const logTarget = (event: MouseEvent) => {
console.log(event.target)
}
</script>
<template>
<p>{{ count }}</p>
<!-- 1. その場に式を書く -->
<button @click="count += 1">式を書く</button>
<!-- 2. 関数名だけ渡す -->
<button @click="increment">関数名を渡す</button>
<!-- 3. 呼び出しの形で書く -->
<button @click="incrementBy(5)">引数を渡す</button>
<!-- 関数名だけ渡すと、イベントオブジェクトが第 1 引数に入る -->
<button @click="logTarget">イベントを受け取る</button>
<!-- 呼び出しの形でイベントも渡したいときは $event を使う -->
<button @click="logTarget($event)">明示的に渡す</button>
</template>
括弧を付けるかどうかで何が変わるか
関数名だけを渡すと、Vue はその関数をハンドラとして登録します。DOM の要素に付けた場合は DOM から呼ばれるので、イベントオブジェクトが第 1 引数に入ります。
括弧を付けると、その式全体がハンドラの中身になります。incrementBy(5) は「5 を渡して呼ぶ」という式なので、イベントオブジェクトはどこにも渡りません。必要なら $event で明示的に渡します。
| 書き方 | 第 1 引数 | 使う場面 |
|---|---|---|
@click="increment" | イベントオブジェクト | 引数がない。イベントを使いたい |
@click="increment()" | なし | 引数がなく、イベントも要らない |
@click="incrementBy(5)" | 渡した引数 | 引数を渡す |
@click="logTarget($event)" | $event で渡す | 引数とイベントの両方が要る |
@click="count += 1" | 引数の概念がない | 短い式で済む。式の中で $event は使える |
@click="increment" と @click="increment()" は結果が同じに見えますが、前者はイベントオブジェクトを受け取ります。ハンドラの引数を後から増やしたときに意図しない値が入るのは、この違いが原因です。
第 1 引数に入るものは、付けた相手が DOM の要素かコンポーネントかで変わります。 自作コンポーネントに @picked="onPicked" と書き、子が emit('picked', 7) で通知するなら、第 1 引数は 7 です。イベントオブジェクトは入りません。同じ書き方でも渡ってくるものが違うので、コンポーネントのイベントを受けるときは子が何を渡しているかを確認します (6 章で扱います)。
なお script setup では this を使いません。ハンドラは普通の関数として定義するので、this の束縛を気にする必要がありません。
イベント修飾子
event.stopPropagation() や event.preventDefault() のような定型処理は、修飾子でテンプレート側に書けます。
<template>
<!-- 伝播を止める -->
<button @click.stop="handle">stop</button>
<!-- 既定動作を止める。フォームのリロードを防ぐ定番 -->
<form @submit.prevent="onSubmit">
<button type="submit">送信</button>
</form>
<!-- ハンドラを書かず修飾子だけでもよい -->
<form @submit.prevent></form>
<!-- event.target が自分自身のときだけ発火する -->
<div @click.self="onSelfClick">
<p>ここをクリックしても発火しない</p>
</div>
<!-- 連結できる -->
<a href="#" @click.stop.prevent="onLink">stop と prevent</a>
</template>
ハンドラの中で event.preventDefault() を呼ぶのと結果は同じですが、修飾子にするとハンドラが DOM の事情から切り離されます。ハンドラは「何をするか」だけを持ち、「どう受けるか」はテンプレートに残ります。
主な修飾子は次のとおりです。
| 修飾子 | 内容 |
|---|---|
.stop | 伝播を止める (stopPropagation) |
.prevent | 既定動作を止める (preventDefault) |
.self | event.target が要素自身のときだけ発火する |
.once | 一度だけ発火する |
.capture | キャプチャフェーズで受ける |
.passive | 既定動作を待たせない |
.once / .capture / .passive はネイティブの addEventListener のオプションに対応します。とくに .passive は、ハンドラの中で preventDefault() を呼ばないと約束することでブラウザに待たせない指定です。スクロールのように高頻度で発生するイベントで効きます。
キー修飾子とシステム修飾子
キーボードのイベントは、修飾子で対象のキーを絞れます。
<script setup lang="ts">
const submit = () => {}
const save = () => {}
</script>
<template>
<!-- Enter で発火する -->
<input @keyup.enter="submit" />
<!-- Enter でも Space でも発火する (連結は「または」) -->
<input @keyup.enter.space="submit" />
<!-- Ctrl を押しながら Enter のときだけ発火する (「かつ」) -->
<input @keyup.ctrl.enter="submit" />
<!-- macOS の Cmd+S と Windows の Ctrl+S を同じ要素で拾う -->
<input @keydown.meta.s.prevent="save" @keydown.ctrl.s.prevent="save" />
</template>
meta が見ているのは event.metaKey だけで、プラットフォームごとの読み替えはしません。macOS の Command キーは metaKey ですが、Windows や Linux の Ctrl は ctrlKey です。両方の環境で同じ操作を拾いたいなら、同じ要素に .meta と .ctrl の 2 つを並べて書きます。 修飾子が違えば同じイベント名を複数書けます。
連結の意味が修飾子の種類で変わります。
enter や space のようなキー修飾子を並べると 「いずれかのキー」 になります。@keyup.enter.space は Enter でも Space でも発火します。
ctrl / alt / shift / meta のシステム修飾子は 「かつ」 です。@keyup.ctrl.enter は、Ctrl を押していない状態で Enter を離しても発火しません。
keyup でシステム修飾子を使う場合、キーを離す時点で修飾キーが押されている必要があります。Ctrl を先に離してから Enter を離すと発火しません。
余分な修飾キーを弾く
システム修飾子は「そのキーが押されていること」しか見ません。他のキーが一緒に押されていても発火します。厳密に一致させたいときは .exact を足します。
<template>
<!-- Alt や Shift を併用しても発火する -->
<button @click.ctrl="onCtrlClick">Ctrl (他が混ざってもよい)</button>
<!-- Ctrl だけのときに発火する -->
<button @click.ctrl.exact="onCtrlOnly">Ctrl だけ</button>
<!-- 修飾キーが 1 つも押されていないときに発火する -->
<button @click.exact="onPlainClick">修飾キーなし</button>
</template>
日本語入力の確定と Enter
Vue はキー修飾子で IME の変換中を判定しません。 イベントに isComposing: true が立っていても、event.key が一致すればハンドラを呼びます。
v-model の側には変換中を判定する仕組みが入っていて、変換開始から確定までの入力イベントを無視します。キー修飾子にはその判定がありません。同じライブラリの中で扱いが違うので、v-model が安全だからキー修飾子も安全だとは言えません。
問題になるのは、変換を確定する Enter がキーイベントとしてどう届くかです。ここはブラウザと IME の組み合わせで変わります。確定の Enter が event.key === 'Enter' として届く環境では、変換を確定したつもりの操作で送信が走ります。
避けるには、ハンドラの中で event.isComposing を見て変換中なら何もしないか、送信をボタンに寄せます。日本語入力のある画面で @keyup.enter を送信に割り当てるなら、対象の環境で実際に試してください。
v-model でフォームと状態をつなぐ
入力欄の値と状態を結びつけるのが v-model です。値の反映とイベントの受け取りをまとめた糖衣構文です。
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('')
const age = ref<number | null>(null)
const isAgreed = ref(false)
const plan = ref('')
const frameworks = ref<string[]>([])
const city = ref('')
</script>
<template>
<!-- テキスト: 文字列が入る -->
<input v-model="name" type="text" />
<!-- チェックボックス (単一): 真偽値が入る -->
<input v-model="isAgreed" type="checkbox" />
<!-- チェックボックス (複数): value の配列になる -->
<label><input v-model="frameworks" type="checkbox" value="vue" /> Vue</label>
<label><input v-model="frameworks" type="checkbox" value="react" /> React</label>
<!-- ラジオ: 選ばれた value が入る -->
<label><input v-model="plan" type="radio" value="free" /> 無料</label>
<label><input v-model="plan" type="radio" value="pro" /> 有料</label>
<!-- セレクト: 選ばれた option の value が入る -->
<select v-model="city">
<option disabled value="">選択してください</option>
<option value="sendai">仙台</option>
<option value="tokyo">東京</option>
</select>
</template>
入力の種類ごとに状態へ入るものが変わります。
| 種類 | 状態に入るもの |
|---|---|
| テキスト、テキストエリア | 入力された文字列 |
| チェックボックス (単一) | true か false |
| チェックボックス (複数を同じ状態に結ぶ) | チェックされた value の配列 |
| ラジオ | 選ばれた value |
| セレクト (単一選択) | 選ばれた option の value |
セレクト (multiple 付き) | 選ばれた option の value の配列 |
複数のチェックボックスを同じ配列に結ぶと、Vue が値の出し入れを引き受けます。自分で配列を操作する必要はありません。
<select multiple> に v-model を付けるときは、結ぶ側も配列で用意します。文字列を渡すと開発ビルドで警告が出ます。
v-model の修飾子
<template>
<!-- change イベントで同期する (既定は input) -->
<input v-model.lazy="name" />
<!-- 数値へ変換する。type="number" では指定しなくても変換される -->
<input v-model.number="age" type="text" />
<!-- 前後の空白を落とす -->
<input v-model.trim="name" />
</template>
| 修飾子 | 内容 |
|---|---|
.lazy | input ではなく change イベントで同期する |
.number | 数値へ変換する |
.trim | 前後の空白を除去する |
.number の変換は parseFloat と同じ規則です。注意点が 2 つあります。
1 つ目は、数値で始まる文字列が部分的に変換される点です。"12abc" は 12 になります。「変換できない」のではなく「読めるところまで読む」ので、想定外の入力が数値として通ります。
2 つ目は、先頭から数値として読めない場合に元の文字列が残る点です。"abc" は "abc" のまま入り、空文字も空文字のまま残ります。型では number を期待していても実際には文字列が入りうるので、送信前の検証は別に必要です。
なお type="number" の入力欄では .number は要りません。Vue が type を見て自動で数値へ変換します。
.lazy は入力中の再描画を減らしたいときに使えます。ただし入力途中の値を見て何かを表示する画面では、反応が遅れて不自然になります。
まとめ
- DOM の要素に関数名だけを渡すとイベントオブジェクトが第 1 引数に入り、括弧を付けると渡りません。必要なら
$eventで明示します - 第 1 引数に入るものは相手が DOM の要素かコンポーネントかで変わります。 コンポーネントのイベントでは
emitの引数が入ります - イベント修飾子を使うと、ハンドラを DOM の事情から切り離せます。ハンドラを書かずに
@submit.preventだけ書く形も使えます .passiveは「preventDefault()を呼ばない」という約束で、既定動作を待たせません- **キー修飾子の連結は「または」、システム修飾子は「かつ」**です。
@keyup.enter.spaceは Enter でも Space でも発火し、@keyup.ctrl.enterは Ctrl を押していないと発火しません keyupでシステム修飾子を使うときは、キーを離す時点で修飾キーが押されている必要があります。余分な修飾キーを弾くには.exactを足します.metaはプラットフォームを読み替えません。 macOS の Command はmetaKey、Windows や Linux の Ctrl はctrlKeyなので、両方拾うには同じ要素に 2 つ並べて書きます- Vue はキー修飾子で IME の変換中を判定しません。
v-model側には判定があるのに、キー修飾子にはありません。確定の Enter がどう届くかは環境次第なので、日本語入力のある画面では実際に試します v-modelが状態へ入れるものは入力の種類で変わります。チェックボックスは単一なら真偽値、複数を同じ状態に結ぶと配列、<select multiple>も配列です.numberはparseFloatの規則です。"12abc"は 12 になり、先頭から読めない場合は元の文字列が残ります。数値である前提で扱う前に検証が必要です
- Vue — イベントハンドリング — 修飾子の一覧と
.exactの説明 - Vue — フォーム入力バインディング — 入力の種類ごとの挙動と修飾子
- React ガイド — イベントハンドリング — 合成イベントを使う React 側との比較
- React ガイド — フォーム — 制御コンポーネントとの対比
次に読む
- 算出プロパティとウォッチャー — 入力から別の値を導く方法
- コンポーネント間の v-model — 自作コンポーネントに
v-modelを持たせる方法