Component Events and v-model on Components

intermediate
14 min

Component Events and v-model on Components

Props carry data down the component tree; emitted events carry information back up. Together they form Vue's one-way data flow: the parent owns state, the child requests changes. In this lesson you will emit custom events, pass payloads, and build components that support v-model -- the pattern behind every reusable form control.

Emitting Events from a Child

Declare events with the defineEmits macro, then call the returned function:

vue
<!-- AddToCartButton.vue --> <script setup> defineProps({ productId: Number }) const emit = defineEmits(['add-to-cart']) </script> <template> <button @click="emit('add-to-cart', productId, 1)"> Add to cart </button> </template>

The parent listens with the same @ syntax it uses for DOM events:

vue
<script setup> import AddToCartButton from './AddToCartButton.vue' function handleAdd(id, qty) { console.log('Add product', id, 'x', qty) } </script> <template> <AddToCartButton :product-id="42" @add-to-cart="handleAdd" /> </template>

Everything after the event name in emit(...) becomes arguments to the listener. Event names are written kebab-case in templates, matching prop conventions.

Validating emitted payloads

Like props, emits support object syntax with validators (dev-mode warnings only):

js
const emit = defineEmits({ 'add-to-cart': (id, qty) => Number.isInteger(id) && qty > 0 })

Why Not Just Mutate a Prop?

If a child silently changed parent data, you could never tell where a value was modified. Emits make every state change traceable: search the parent for @event-name and you find the single function responsible. Vue Devtools even logs every emitted event with its payload.

v-model on Components

On a native input, v-model="text" is sugar for :value="text" plus @input. On a component, Vue 3.4+ gives you the elegant defineModel macro:

vue
<!-- SearchInput.vue --> <script setup> const model = defineModel() // a ref synced with the parent </script> <template> <input :value="model" @input="model = $event.target.value" class="search"> </template>

The parent simply writes:

vue
<SearchInput v-model="query" />

Reading model gives the parent's value; assigning to it emits the update automatically. Under the hood this is still a modelValue prop plus an update:modelValue event -- the pre-3.4 pattern you may meet in older codebases:

js
const props = defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) // emit('update:modelValue', newValue)

Multiple and named models

A component can expose several two-way bindings:

vue
<!-- UserForm.vue --> <script setup> const firstName = defineModel('firstName') const lastName = defineModel('lastName') </script>
vue
<UserForm v-model:first-name="first" v-model:last-name="last" />

A Complete Example: Star Rating

vue
<!-- StarRating.vue --> <script setup> const rating = defineModel({ type: Number, default: 0 }) const stars = [1, 2, 3, 4, 5] </script> <template> <span v-for="s in stars" :key="s" @click="rating = s" :style="{ cursor: 'pointer', color: s <= rating ? 'gold' : '#ccc' }"> </span> </template>

Any parent can now write <StarRating v-model="review.score" /> and the score stays perfectly in sync -- a genuinely reusable form control in a dozen lines.

Key Takeaways

  • Children communicate upward with defineEmits and emit('event', ...payload).
  • Parents listen with @event-name, receiving the payload as arguments.
  • Never mutate props; emit an event and let the owner update the state.
  • defineModel() (Vue 3.4+) makes custom components work with v-model in one line.
  • Named models (v-model:first-name) allow multiple two-way bindings per component.

Next up: Slots and Content Distribution -- passing template content, not just data, into components.

Component Events and v-model on Components - Vue.js | CodeYourCraft | CodeYourCraft