Emitting Events

Chapter 7
Emitting Events and v-model on Components
How a child talks back to its parent — and building a component that supports two-way binding

Props (Chapter 6) flow data down into a child. To send something back up — a click, a deletion, a new value — a child emits an event that the parent listens for. This is Vue's counterpart to React's "pass a callback down" and Angular's @Output/EventEmitter. Declared with the defineEmits macro.

Emitting an Event with defineEmits

<!-- TodoItem.vue --> <script setup> const props = defineProps(['id', 'text']); const emit = defineEmits(['delete']); </script> <template> <li> {{ text }} <button @click="emit('delete', id)">Delete</button> </li> </template>
<!-- parent template --> <TodoItem :id="todo.id" :text="todo.text" @delete="removeTodo" />

defineEmits(['delete']) declares which events this component can emit and returns an emit function. Calling emit('delete', id) fires the delete event with id as its payload. The parent listens with the @ event syntax from Chapter 3 — @delete="removeTodo" — and removeTodo receives the emitted id as its argument. Structurally identical to Angular's @Output, and the same role as a React callback prop, but modeled as a custom event the child raises (like Angular, unlike React).

Custom events read like native ones
Because the parent listens with @delete — the same syntax as @click — custom events are conventionally named as plain events (delete, save, change), not onDelete. From the parent's perspective <TodoItem @delete="..." /> reads just like listening to a built-in element's event. (This mirrors Angular's @Output naming convention exactly.)

Multiple Events and Validated Emits

const emit = defineEmits(['save', 'cancel']); // or the object form, documenting each event's payload: const emit = defineEmits({ save: (payload) => typeof payload === 'object', // returns true if valid cancel: null, // no validation });

A component can declare several events in the array. The object form additionally lets you validate each event's payload with a function (Vue warns if it returns false) — useful documentation of what each event carries, comparable to typing an Angular EventEmitter<T>.

v-model on a Component

Chapter 3 used v-model on a native <input>. You can also put v-model on a custom component, to build something like a reusable input control with two-way binding. Under the hood, v-model on a component is shorthand for a modelValue prop plus an update:modelValue event:

<!-- CustomInput.vue --> <script setup> defineProps(['modelValue']); const emit = defineEmits(['update:modelValue']); </script> <template> <input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" /> </template>
<!-- parent: v-model just works --> <CustomInput v-model="name" />

The child takes the current value via the modelValue prop, binds it to its real input with :value, and emits update:modelValue whenever the input changes. The parent then uses plain v-model="name" — Vue wires the prop and event together automatically. This is the exact convention Angular uses for custom two-way binding (value + valueChange), and it's why v-model on native inputs "just works": those are built on the same mechanism.

defineModel — the newer shorthand
Recent Vue versions (3.4+) add defineModel(), which collapses the whole modelValue-prop-plus-event pattern above into a single line: const model = defineModel(), then bind v-model="model" on the inner input. It's the recommended modern approach for new code — worth knowing the longer form above too, since it's what defineModel expands to and what you'll see in slightly older code.
A child still can't mutate the prop directly
Even when building a v-model component, the child must not reassign modelValue directly — it emits update:modelValue and lets the parent update the source of truth, which flows back down as the new prop. The one-way-data-flow discipline from Chapter 6 holds: data down via props, changes up via events.
VueAngularReact
defineEmits(['delete'])@Output() deleteA callback prop (onDelete)
emit('delete', id)this.delete.emit(id)onDelete(id)
@delete="handler"(delete)="handler($event)"onDelete={handler}
v-model on component[(value)] (value + valueChange)value prop + onChange prop

Coding Challenges

Challenge 1

Build a TodoItem component with id/text props and a delete emit. The parent holds an array of todos, renders one TodoItem each, and removes the matching todo when a delete event fires.

📄 View solution
Challenge 2

Build a CustomInput component supporting v-model via a modelValue prop and an update:modelValue emit, then use v-model on it in a parent and display the bound value live.

📄 View solution
Challenge 3

Rewrite Challenge 2's CustomInput using the newer defineModel() shorthand, confirming the parent's v-model usage is unchanged.

📄 View solution

Chapter 7 Quick Reference

  • defineEmits([...]) — declares emittable events; returns an emit function
  • emit('event', payload) — fires an event upward; parent listens with @event="handler"
  • Name events plainly (delete, save), not onX — they read like native events
  • v-model on a component = a modelValue prop + an update:modelValue event
  • defineModel() — the modern one-line shorthand for that whole pattern (Vue 3.4+)
  • The child never mutates the prop — it emits, and the parent updates the source of truth
  • Next chapter: slots — Vue's content-projection / composition mechanism