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> ```