Props go down; events come up. A child never reaches into its parent — it announces that something happened and lets the parent decide what that means.
defineEmits
const emit = defineEmits(["like", "comment", "share", "view", "toggle-mute"]);Like defineProps, this is a compiler macro — no import, top level only. It returns
the function you call to emit:
function remove(tag) {
emit("update:modelValue", props.modelValue.filter((t) => t !== tag));
}The parent listens with @:
<ReelPlayer
:reel="reel"
:active="index === activeIndex"
@like="onLike(reel)"
@view="onView"
@share="onShare(reel)"
/>Note the two forms. @view="onView" passes the handler, so it receives the emitted
payload as its argument. @like="onLike(reel)" is an inline expression, so it calls
onLike with the reel instead — and the payload is available as $event if you
need it too.
Declaring events is optional but worth it
emit("like") works whether or not you declared it. Declaring is worth the line for
three reasons: the component's interface is documented in one place, tooling can check it, and a
declared event is removed from $attrs — so it does not also get bound as
a native DOM listener on the root element, which causes a genuinely puzzling double-fire.
Event modifiers
The small pieces of glue that every form needs. Instead of calling
event.preventDefault() in the handler, say it in the template:
<form @submit.prevent="submit">The useful ones:
.prevent — preventDefault(). Almost every @submit wants
it.
.stop — stopPropagation(). A button inside a clickable card.
.self — only fire if the event target is this element, not a descendant. This is how
the demo application's modal backdrop works:
<div class="position-fixed ..." @click.self="confirmDelete = null">Clicking the dark backdrop closes the dialog; clicking the dialog itself does not, because the
event target is then a child. Without .self every click inside the modal would dismiss
it.
.once — remove the listener after the first fire.
.passive — tells the browser you will not call preventDefault(), so it
need not wait before scrolling. Worth it on @touchmove and @wheel.
They chain, and order matters: @click.prevent.self prevents all
clicks and then filters, while @click.self.prevent filters first and only prevents its
own.
Key modifiers
For keyboard events, listen for a specific key by name:
@keydown.enter.prevent="add"
@keydown.,.prevent="add"
@keydown.delete="onBackspace"Three keys, three behaviours, no if (event.key === ...) anywhere. Enter and comma both
commit a tag — and both need .prevent, Enter because it would submit the surrounding
form and comma because it would otherwise be typed into the box. Backspace is handled without
.prevent, because deleting a character is exactly what should happen when the box is not
empty.
Any valid KeyboardEvent.key works, kebab-cased: .esc,
.tab, .delete, .space, .arrow-up,
.page-down. System modifiers are available too — .ctrl,
.shift, .alt, .meta — so @keydown.meta.enter is a
submit shortcut in one attribute.
Payloads
Emit whatever the parent needs, and prefer the minimum:
emit("view", props.reel.id); // an id
emit("update:modelValue", [...props.modelValue, tag]); // a new array
emit("select", { reel, index }); // several values in one objectEmitting a whole object when the parent only needs an id couples the two components together for no benefit. Emitting several positional arguments is legal but hard to read at the call site — put them in an object.
One rule about arrays and objects: emit a new one, do not mutate the old one.
TagInput emits [...props.modelValue, tag] rather than pushing, because the
array belongs to the parent and mutating it is the props-are-read-only violation from lesson 10
wearing a disguise.
Why not just pass a callback prop?
You can — :on-like="handleLike" is a function like any other value, and it is how React
does this. Events are preferred in Vue because they compose with modifiers, they are declared as part
of the component's interface, and a parent can attach several listeners to one event. Use a callback
prop when you need a return value, which an emit cannot give you.
Next: v-model and Two-Way Binding — which is exactly a prop and an event, with sugar.