Emitting Events to Parents

+15 Mana ✨

While props flow data down from parent to child, events flow up from child to parent. This is how children communicate changes back to their parents.

Defining Events

Use defineEmits() to declare what events a component can emit:

vue
<!-- src/components/CounterButton.vue -->
<script setup lang="ts">
const emit = defineEmits<{
  increment: []  // No payload
  decrement: []  // No payload
  change: [newValue: number]  // With payload
}>()

function handleIncrement() {
  emit('increment')
}

function handleDecrement() {
  emit('decrement')
}
</script>

<template>
  <div class="counter-buttons">
    <button @click="handleDecrement">-</button>
    <button @click="handleIncrement">+</button>
  </div>
</template>

Listening to Events

Parents listen with @event-name:

vue
<!-- Parent component -->
<script setup lang="ts">
import CounterButton from './components/CounterButton.vue'
import { ref } from 'vue'

const count = ref(0)

function handleIncrement() {
  count.value++
}

function handleDecrement() {
  count.value--
}
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <CounterButton 
      @increment="handleIncrement" 
      @decrement="handleDecrement" 
    />
  </div>
</template>

Events with Payloads

Pass data with emitted events:

vue
<!-- src/components/SearchBox.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const emit = defineEmits<{
  search: [query: string]
  clear: []
}>()

const query = ref('')

function handleSubmit() {
  if (query.value.trim()) {
    emit('search', query.value.trim())
  }
}

function handleClear() {
  query.value = ''
  emit('clear')
}
</script>

<template>
  <form @submit.prevent="handleSubmit" class="search-box">
    <input 
      v-model="query" 
      placeholder="Search..." 
    />
    <button type="submit">Search</button>
    <button type="button" @click="handleClear">Clear</button>
  </form>
</template>
vue
<!-- Parent component -->
<script setup lang="ts">
import SearchBox from './components/SearchBox.vue'
import { ref } from 'vue'

const searchResults = ref<string[]>([])

function handleSearch(query: string) {
  console.log('Searching for:', query)
  // Perform search...
}

function handleClear() {
  searchResults.value = []
}
</script>

<template>
  <SearchBox 
    @search="handleSearch" 
    @clear="handleClear" 
  />
</template>

Event Naming Convention

Use kebab-case for event names in templates:

vue
<!-- Child component -->
<script setup lang="ts">
const emit = defineEmits<{
  itemSelected: [id: number]  // camelCase in TypeScript
  updateValue: [value: string]
}>()

emit('itemSelected', 42)
emit('updateValue', 'hello')
</script>

<!-- Parent component template -->
<template>
  <!-- Use kebab-case when listening -->
  <ChildComponent 
    @item-selected="handleSelect"
    @update-value="handleUpdate"
  />
</template>

Inline Event Handlers

For simple cases, use inline handlers:

vue
<template>
  <!-- Access payload with $event -->
  <SearchBox @search="searchQuery = $event" />
  
  <!-- Or inline function -->
  <SearchBox @search="(q) => performSearch(q)" />
  
  <!-- Multiple arguments -->
  <DataTable @sort="(column, direction) => sortBy(column, direction)" />
</template>

Complete Example: Todo Item

vue
<!-- src/components/TodoItem.vue -->
<script setup lang="ts">
type Todo = {
  id: number
  text: string
  done: boolean
}

const props = defineProps<{
  todo: Todo
}>()

const emit = defineEmits<{
  toggle: [id: number]
  delete: [id: number]
  edit: [id: number, newText: string]
}>()

import { ref } from 'vue'

const isEditing = ref(false)
const editText = ref('')

function startEdit() {
  editText.value = props.todo.text
  isEditing.value = true
}

function saveEdit() {
  if (editText.value.trim()) {
    emit('edit', props.todo.id, editText.value.trim())
  }
  isEditing.value = false
}

function cancelEdit() {
  isEditing.value = false
}
</script>

<template>
  <li class="todo-item" :class="{ done: todo.done }">
    <input 
      type="checkbox" 
      :checked="todo.done"
      @change="emit('toggle', todo.id)"
    />
    
    <template v-if="isEditing">
      <input 
        v-model="editText" 
        @keyup.enter="saveEdit"
        @keyup.esc="cancelEdit"
      />
      <button @click="saveEdit">Save</button>
      <button @click="cancelEdit">Cancel</button>
    </template>
    
    <template v-else>
      <span @dblclick="startEdit">{{ todo.text }}</span>
      <button @click="emit('delete', todo.id)">Delete</button>
    </template>
  </li>
</template>

<style scoped>
.todo-item {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.5rem;
}

.done span {
  text-decoration: line-through;
  color: #888;
}
</style>
vue
<!-- Parent: TodoList.vue -->
<script setup lang="ts">
import TodoItem from './TodoItem.vue'
import { ref } from 'vue'

const todos = ref([
  { id: 1, text: 'Learn Vue', done: true },
  { id: 2, text: 'Build an app', done: false },
])

function toggleTodo(id: number) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.done = !todo.done
}

function deleteTodo(id: number) {
  todos.value = todos.value.filter(t => t.id !== id)
}

function editTodo(id: number, newText: string) {
  const todo = todos.value.find(t => t.id === id)
  if (todo) todo.text = newText
}
</script>

<template>
  <ul>
    <TodoItem
      v-for="todo in todos"
      :key="todo.id"
      :todo="todo"
      @toggle="toggleTodo"
      @delete="deleteTodo"
      @edit="editTodo"
    />
  </ul>
</template>
✓ Completed