本文へジャンプ
上に戻る

コンポーネントのイベント

このページは、すでにコンポーネントの基礎を読んでいることを前提にしています。初めてコンポーネントに触れる方は、まずそちらをお読みください。

イベントの発行と購読

コンポーネントは、組み込みの $emit メソッドを使用して、テンプレート式(例: v-on ハンドラー内)で直接カスタムイベントを発行できます:

template
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

$emit() メソッドは、コンポーネントインスタンス上でも this.$emit() として使用できます:

js
export default {
  methods: {
    submit() {
      this.$emit('someEvent')
    }
  }
}

そして、親コンポーネントは 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
methods: {
  increaseCount(n) {
    this.count += n
  }
}
js
function increaseCount(n) {
  count.value += n
}

TIP

$emit() に渡されたイベント名の後にあるすべての追加の引数はリスナーに転送されます。たとえば $emit('foo', 1, 2, 3) とすると、リスナー関数は 3 つの引数を受け取ります。

発行するイベントの宣言

発行するイベントは、defineEmits() マクロemits オプションによってコンポーネント上で明示的に宣言できます:

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')
  }
}
js
export default {
  emits: ['inFocus', '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>

詳細: コンポーネントの emit の型付け

js
export default {
  emits: {
    submit(payload) {
    // バリデーションの合格/不合格を示す
    // `true` または `false` を返す
    }
  }
}

参照: コンポーネントの emit の型付け

任意ですが、コンポーネントがどのように動作すべきかをよりよく文書化するために、発行されるすべてのイベントを定義することが推奨されます。また、これにより Vue は既知のリスナーをフォールスルー属性から除外し、サードパーティのコードによって手動でディスパッチされた DOM イベントによって起こるエッジケースを回避できます。

TIP

ネイティブイベント(例: click)が emits オプションに定義されている場合、リスナーはコンポーネントが発行する click イベントのみを購読し、ネイティブの click イベントには反応しなくなります。

イベントのバリデーション

発行するイベントは、プロパティの型バリデーションと同様に、配列構文ではなくオブジェクト構文で定義されている場合にバリデーションできます。

バリデーションを追加するには、「this.$emitemit の呼び出しに渡された引数」を受け取り、「イベントが正当かどうかを示すブール値」を返す関数をイベントに割り当てます。

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>
js
export default {
  emits: {
    // バリデーションなし
    click: null,

  // submit イベントをバリデーション
    submit: ({ email, password }) => {
      if (email && password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  },
  methods: {
    submitForm(email, password) {
      this.$emit('submit', { email, password })
    }
  }
}

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 つのことをしなければなりません:

  1. ネイティブの <input> 要素の value 属性を、modelValue プロパティにバインドする
  2. ネイティブの input イベントがトリガーされたら、新しい値で update:modelValue カスタムイベントを発行する

実際には次のようになります:

vue
<!-- CustomInput.vue -->
<script>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue']
}
</script>

<template>
  <input
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>
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>
export default {
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
</script>

<template>
  <input v-model="value" />
</template>
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>

プレイグラウンドで試す

vue
<!-- MyComponent.vue -->
<script>
export default {
  props: ['title'],
  emits: ['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>

プレイグラウンドで試す

vue
<script>
export default {
  props: {
    firstName: String,
    lastName: String
  },
  emits: ['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>
vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  created() {
    console.log(this.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>

プレイグラウンドで試す

vue
<script>
export default {
  props: {
    modelValue: String,
    modelModifiers: {
      default: () => ({})
    }
  },
  emits: ['update:modelValue'],
  methods: {
    emitValue(e) {
      let value = e.target.value
      if (this.modelModifiers.capitalize) {
        value = value.charAt(0).toUpperCase() + value.slice(1)
      }
      this.$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 }
js
export default {
  props: ['title', 'titleModifiers'],
  emits: ['update:title'],
  created() {
    console.log(this.titleModifiers) // { capitalize: true }
  }
}
コンポーネントのイベントが読み込まれました