コンポーネントのイベント
このページは、すでにコンポーネントの基礎を読んでいることを前提にしています。初めてコンポーネントに触れる方は、まずそちらをお読みください。
イベントの発行と購読
コンポーネントは、組み込みの $emit
メソッドを使用して、テンプレート式(例: v-on
ハンドラー内)で直接カスタムイベントを発行できます:
template
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>
そして、親コンポーネントは v-on
を使ってイベントを購読できます:
template
<MyComponent @some-event="callback" />
.once
修飾子は、コンポーネントのイベントリスナーでもサポートされています:
template
<MyComponent @some-event.once="callback" />
コンポーネントや props と同様に、イベント名も自動的な大文字・小文字の変換を提供します。キャメルケースのイベントを発行しましたが、親ではケバブケースのリスナーを使用して購読できることに注意してください。プロパティ名での大文字・小文字の使い分けと同様に、テンプレートではケバブケースのイベントリスナーを使用することをお勧めします。
TIP
ネイティブの DOM イベントとは異なり、コンポーネントから発行されたイベントはバブリングしません。直接の子コンポーネントから発行されたイベントのみを購読できます。兄弟コンポーネントや深くネストしたコンポーネント間で通信する必要がある場合は、外部のイベントバスやグローバルな状態管理ソリューションを使ってください。
イベントの引数
イベントで特定の値を発行すると便利な場合があります。例えば、 <BlogPost>
コンポーネントに、テキストをどれだけ拡大するかを担当させたい場合があります。そのような場合、$emit
に追加の引数を渡して値を提供できます:
template
<button @click="$emit('increaseBy', 1)">
Increase by 1
</button>
次に、親でイベントを購読する際に、リスナーとしてインラインのアロー関数を使用することで、イベントの引数にアクセスできます:
template
<MyButton @increase-by="(n) => count += n" />
または、イベントハンドラーがメソッドの場合は:
template
<MyButton @increase-by="increaseCount" />
その値はそのメソッドの最初のパラメーターとして渡されます:
js
function increaseCount(n) {
count.value += n
}
TIP
$emit()
に渡されたイベント名の後にあるすべての追加の引数はリスナーに転送されます。たとえば $emit('foo', 1, 2, 3)
とすると、リスナー関数は 3 つの引数を受け取ります。
発行するイベントの宣言
発行するイベントは、defineEmits()
マクロ によってコンポーネント上で明示的に宣言できます:
vue
<script setup>
defineEmits(['inFocus', 'submit'])
</script>
<template>
で使用した $emit
メソッドは、コンポーネントの <script setup>
セクション内ではアクセスできませんが、代わりに defineEmits()
が同等の関数を返してくれるので、それを使用できます:
vue
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
defineEmits()
マクロは関数の中では使用できません。上記の例のように、<script setup>
内に直接記述する必要があります。
<script setup>
の代わりに明示的な setup
関数を使う場合は、イベントは emits
オプションを使って宣言する必要があり、emit
関数は setup()
コンテキスト上で公開されます:
js
export default {
emits: ['inFocus', 'submit'],
setup(props, ctx) {
ctx.emit('submit')
}
}
setup()
コンテキストの他のプロパティと同様に、emit
は安全に分割代入できます:
js
export default {
emits: ['inFocus', 'submit'],
setup(props, { emit }) {
emit('submit')
}
}
emits
オプションはオブジェクト構文もサポートしており、発行されたイベントのペイロードのランタイムバリデーションを実行できます:
vue
<script setup>
const emit = defineEmits({
submit(payload) {
// バリデーションの合格/不合格を示す
// `true` または `false` を返す
}
})
</script>
<script setup>
で TypeScript 使用している場合、純粋な型アノテーションを使用して、発行するイベントを宣言することもできます:
vue
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
</script>
任意ですが、コンポーネントがどのように動作すべきかをよりよく文書化するために、発行されるすべてのイベントを定義することが推奨されます。また、これにより Vue は既知のリスナーをフォールスルー属性から除外し、サードパーティのコードによって手動でディスパッチされた DOM イベントによって起こるエッジケースを回避できます。
TIP
ネイティブイベント(例: click
)が emits
オプションに定義されている場合、リスナーはコンポーネントが発行する click
イベントのみを購読し、ネイティブの click
イベントには反応しなくなります。
イベントのバリデーション
発行するイベントは、プロパティの型バリデーションと同様に、配列構文ではなくオブジェクト構文で定義されている場合にバリデーションできます。
バリデーションを追加するには、「emit
の呼び出しに渡された引数」を受け取り、「イベントが正当かどうかを示すブール値」を返す関数をイベントに割り当てます。
vue
<script setup>
const emit = defineEmits({
// バリデーションなし
click: null,
// submit イベントをバリデーション
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
function submitForm(email, password) {
emit('submit', { email, password })
}
</script>
v-model
での使用
カスタムイベントは v-model
で動作するカスタム入力を作成するためにも使用できます。ここで、ネイティブ要素で v-model
がどのように使われるかを再確認してみましょう:
template
<input v-model="searchText" />
テンプレートコンパイラーはその内部で、 v-model
を冗長な同じ内容に展開してくれます。つまり、上のコードは以下と同じことをするわけです:
template
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
コンポーネントで使用する場合はその代わり、v-model
は以下のように展開されます:
template
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
しかし、これを実際に動作させるためには、<CustomInput>
コンポーネントは次の 2 つのことをしなければなりません:
- ネイティブの
<input>
要素のvalue
属性を、modelValue
プロパティにバインドする - ネイティブの
input
イベントがトリガーされたら、新しい値でupdate:modelValue
カスタムイベントを発行する
実際には次のようになります:
vue
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
これで v-model
はこのコンポーネントで完全に動作するはずです:
template
<CustomInput v-model="searchText" />
このコンポーネントで v-model
を実装するもう 1 つの方法は、getter と setter の両方を持つ、書き込み可能な computed
プロパティを使用することです。get
メソッドは modelValue
プロパティを返し、set
メソッドは対応するイベントを発行する必要があります:
vue
<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<template>
<input v-model="value" />
</template>
v-model
の引数
デフォルトでは、コンポーネントの v-model
は、プロパティとして modelValue
を、イベントとして update:modelValue
を使用します。これらの名前は、v-model
に引数として渡すことで変更できます:
template
<MyComponent v-model:title="bookTitle" />
この場合、子コンポーネントは title
プロパティを受け取り、 update:title
イベントを発行して親の値を更新する必要があります:
vue
<!-- MyComponent.vue -->
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
複数の v-model
のバインディング
先ほど v-model
の引数で学んだように、特定のプロパティとイベントをターゲットにする機能を活用することで、1 つのコンポーネントインスタンスに複数の v-model バインディングを作成できるようになりました。
各 v-model は、コンポーネントで追加のオプションを必要とせずに、別のプロパティに同期します:
template
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
vue
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
<template>
<input
type="text"
:value="firstName"
@input="$emit('update:firstName', $event.target.value)"
/>
<input
type="text"
:value="lastName"
@input="$emit('update:lastName', $event.target.value)"
/>
</template>
v-model
修飾子の処理
フォームの入力バインディングについて学習しているときに、v-model
には 組み込みの修飾子(.trim
, .number
, .lazy
)があることを確認しました。場合によっては、カスタム入力コンポーネントの v-model
でカスタム修飾子をサポートしたいかもしれません。
カスタム修飾子の例として、v-model
バインディングによって提供される文字列の最初の文字を大文字にする capitalize
を作成してみましょう:
template
<MyComponent v-model.capitalize="myText" />
コンポーネント v-model
に追加された修飾子は、modelModifiers
プロパティを通じてコンポーネントに提供されます。以下の例では、modelModifiers
プロパティを含むコンポーネントを作成しています。これはデフォルトでは空のオブジェクトです:
vue
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
defineEmits(['update:modelValue'])
console.log(props.modelModifiers) // { capitalize: true }
</script>
<template>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
コンポーネントの modelModifiers
プロパティに capitalize
が含まれており、その値が true
であることに注目してください。これは、v-model
バインディングに v-model.capitalize="myText"
が設定されているためです。
これでプロパティの設定ができたので、modelModifiers
オブジェクトのキーをチェックして、発行された値を変更するハンドラーを書くことができます。以下のコードでは、<input />
要素が input
イベントを発火するたびに、文字列を大文字にしています。
vue
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
引数と修飾子の両方を持つ v-model
バインディングの場合、生成されるプロパティの名前は arg + "Modifiers"
になります。例えば:
template
<MyComponent v-model:title.capitalize="myText">
対応する宣言は次のとおりです:
js
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])
console.log(props.titleModifiers) // { capitalize: true }