The Problem
Vue's v-model on custom components requires the component to emit an update:modelValue event. If this event is not emitted correctly, the parent's bound value never updates.
Symptoms
- Input in custom component changes visually but parent value stays the same
- v-model appears one-way: parent to child works, child to parent does not
- Form submission sends old values
- No error in console, just silent failure
Real Error Scenario
```vue <!-- Parent.vue --> <template> <CustomInput v-model="searchTerm" /> <p>Searching for: {{ searchTerm }}</p> <!-- Never updates --> </template>
<script setup> const searchTerm = ref(''); </script>
<!-- CustomInput.vue (WRONG) --> <template> <input :value="modelValue" @input="$emit('input', $event.target.value)" /> </template>
<script setup> defineProps(['modelValue']); defineEmits(['input']); // WRONG event name! </script> ```
Vue 3 expects update:modelValue, not input.
How to Fix It
Fix 1: Vue 3 Correct emit
```vue <!-- CustomInput.vue (CORRECT) --> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template>
<script setup> defineProps(['modelValue']); defineEmits(['update:modelValue']); </script> ```
Fix 2: Using defineModel (Vue 3.4+)
```vue <!-- CustomInput.vue (Vue 3.4+) --> <template> <input v-model="model" /> </template>
<script setup> const model = defineModel(); // Automatically handles props + emit </script> ```
Fix 3: Multiple v-models
```vue <!-- Parent --> <SearchInput v-model:query="searchTerm" v-model:filters="activeFilters" />
<!-- SearchInput.vue --> <script setup> const query = defineModel('query'); const filters = defineModel('filters'); </script>
<template> <input v-model="query" placeholder="Search..." /> <FilterPanel v-model="filters" /> </template> ```
Fix 4: With Validation
```vue <!-- CustomInput.vue --> <script setup> const props = defineProps({ modelValue: { type: String, default: '' }, maxLength: { type: Number, default: 100 } });
const emit = defineEmits(['update:modelValue']);
function onInput(event) { const value = event.target.value.slice(0, props.maxLength); emit('update:modelValue', value); } </script>
<template> <input :value="modelValue" @input="onInput" :maxlength="maxLength" /> </template> ```