| |

Vue.js 33 ๐ŸŸข Multiple v-model Bindings on a Single Component

A component often needs more than one two-way binding. A form component has a first name and a last name. A date-range picker has a start date and an end date. A filter panel has a search term and a category. A settings component has a theme and a language. Each of these is a value that the parent passes down and the child updates. In Vue 3, each binding is a separate v-model directive with a name, and the component declares a prop and an event for each one. The syntax is v-model:firstName="first", and the component declares the firstName prop and the update:firstName event. The parent can bind any number of them, and the component’s interface stays explicit.

The mechanism is the same as the single v-model, repeated. The default v-model uses the modelValue prop and the update:modelValue event. The named v-model:firstName uses the firstName prop and the update:firstName event. The defineModel macro declares each binding with a single call: const firstName = defineModel<string>('firstName') returns a ref that is wired to the prop and the event. The manual approach uses the defineProps and the defineEmits with the matching names. The two approaches produce the same result, and the choice is a matter of the Vue version and the preference.

The multiple bindings are independent. Each one has its own prop, its own event, and its own value. The parent can bind all of them, or only the ones it needs. The child can emit an update for one binding without affecting the others. The independence is the point: the component’s interface is a set of named bindings, and each one is a separate channel between the parent and the child.

This chapter covers three areas. First, why multiple v-model bindings exist โ€” the multi-value component problem and the named-binding solution. Second, how to implement them โ€” the manual defineProps/defineEmits approach, the defineModel macro, and the naming rules. Third, the advanced patterns โ€” the modifiers for the named bindings, the writable computed for the transformations, and the complete components that combine the multiple bindings with the other features. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the binding flow.

Key point: A component can have any number of v-model bindings. The default uses modelValue and update:modelValue. The named bindings use v-model:name with the name prop and the update:name event. The defineModel('name') macro declares the named binding. The bindings are independent, and each one has its own prop, event, and value.


Why multiple v-model bindings exist

The multi-value problem. A component that represents a form, a filter, or a settings panel has several values that the parent needs to read and write. The alternative to the multiple v-model bindings is a single object prop that carries all the values, with a single event that carries the updated object. The object approach works, but it is less ergonomic: the parent binds a single object, and the child emits a new object with all the values, even if only one changed. The multiple bindings make each value a separate channel, and the parent binds each one independently.

The explicitness problem. A single object prop hides the component’s interface. The parent does not know which fields the component uses, and the child does not know which fields the parent cares about. The multiple bindings make the interface explicit: the props are the bindings, and the events are the updates. The parent reads the component’s signature and knows exactly which values it can bind.

The independence problem. The multiple bindings are independent. The child emits an update for the firstName binding without touching the lastName. The parent can bind the firstName to a ref and the lastName to a different ref, and the two are unrelated. The independence is the same as the separate refs in the parent: each binding is a separate piece of state.

The naming problem. The name of each binding is the argument in the v-model:name directive. The name is the prop name, and the event name is the update: prefix plus the name. The component declares the prop and emits the event with the matching names. The naming is the contract, and the component’s author chooses the names.

The modifier problem. Each binding can have its own modifier. The v-model:firstName.trim populates the firstNameModifiers prop, and the component applies the trim to the first name only. The modifiers are per-binding, and the component reads the nameModifiers prop for each one.

The trade-off. The multiple bindings add the verbosity to the template. A component with four bindings has four v-model: directives, and the parent’s template is longer. The alternative โ€” a single object prop โ€” is shorter but less explicit. The trade-off is between the verbosity of the explicit bindings and the compactness of the object prop. For the components with a small number of the values, the explicit bindings are the better choice.


a. The manual implementation

The manual implementation uses the defineProps and the defineEmits with the matching names. Each binding has a prop and an event.

<!-- UserForm.vue -->
<script setup lang="ts">
defineProps<{
  firstName: string;
  lastName: string;
  email: string;
}>();

const emit = defineEmits<{
  'update:firstName': [value: string];
  'update:lastName': [value: string];
  'update:email': [value: string];
}>();

function updateField(field: 'firstName' | 'lastName' | 'email', value: string) {
  if (field === 'firstName') emit('update:firstName', value);
  if (field === 'lastName') emit('update:lastName', value);
  if (field === 'email') emit('update:email', value);
}
</script>

<template>
  <input
    :value="firstName"
    @input="updateField('firstName', ($event.target as HTMLInputElement).value)"
  />
  <input
    :value="lastName"
    @input="updateField('lastName', ($event.target as HTMLInputElement).value)"
  />
  <input
    :value="email"
    @input="updateField('email', ($event.target as HTMLInputElement).value)"
  />
</template>

The component declares three props and three events. The updateField function emits the correct event based on the field name. The template binds each prop to the corresponding input.

The parent uses the component with the three named bindings:

<script setup>
import { ref } from 'vue';
import UserForm from './UserForm.vue';

const first = ref('');
const last = ref('');
const email = ref('');
</script>

<template>
  <UserForm
    v-model:firstName="first"
    v-model:lastName="last"
    v-model:email="email"
  />
  <p>{{ first }} {{ last }} ({{ email }})</p>
</template>

The v-model:firstName="first" expands to :firstName="first" and @update:firstName="first = $event". The other two bindings expand the same way. The three refs are updated independently.

The prop readonly. The component cannot mutate the props. The updateField function emits the event, and the parent updates its state. The prop is the value, and the event is the update.

The template binding. The :value="firstName" binds the prop, and the @input listens to the change. The handler emits the correct event with the new value.


b. The defineModel macro

The defineModel macro declares a named binding with a single call. The defineModel('firstName') returns a ref that is wired to the firstName prop and the update:firstName event.

<!-- UserForm.vue with defineModel -->
<script setup lang="ts">
const firstName = defineModel<string>('firstName');
const lastName = defineModel<string>('lastName');
const email = defineModel<string>('email');
</script>

<template>
  <input v-model="firstName" />
  <input v-model="lastName" />
  <input v-model="email" />
</template>

The defineModel<string>('firstName') declares the firstName binding. The returned ref is writable: assigning to it emits the update:firstName event. The v-model="firstName" on the input binds the ref, and the input’s change flows through the ref to the parent.

The options. The defineModel macro accepts the options: the default and the required. The options are the second argument for the named bindings.

const firstName = defineModel<string>('firstName', { default: '' });
const email = defineModel<string>('email', { required: true });

The modifiers. The modifier prop for the named binding is the nameModifiers. The component declares the prop and reads it.

<script setup lang="ts">
const firstName = defineModel<string>('firstName');
const props = defineProps<{
  firstNameModifiers?: { capitalize?: boolean };
}>();
</script>

The v-model:firstName.capitalize populates the firstNameModifiers prop with { capitalize: true }. The component applies the modifier.

The type. The defineModel<string> declares the type as string. The type can be a union, an interface, or any TypeScript type. The type is used for the prop’s type and the event’s payload type.


c. The advanced patterns

The writable computed for the transformations. A binding that needs to transform the value before emitting uses a writable computed.

<script setup lang="ts">
import { computed } from 'vue';

const props = defineProps<{
  firstName: string;
}>();
const emit = defineEmits<{
  'update:firstName': [value: string];
}>();

const firstName = computed({
  get: () => props.firstName,
  set: (value) => emit('update:firstName', value.trim()),
});
</script>

<template>
  <input v-model="firstName" />
</template>

The firstName computed reads from the prop and emits the trimmed value. The transformation happens in the setter, and the parent receives the trimmed value.

The per-binding modifiers. Each binding has its own modifier prop. The component reads the prop for each binding and applies the modifier.

<script setup lang="ts">
const firstName = defineModel<string>('firstName');
const props = defineProps<{
  firstNameModifiers?: { capitalize?: boolean };
  emailModifiers?: { lowercase?: boolean };
}>();

function handleFirstName(event: Event) {
  const target = event.target as HTMLInputElement;
  let value = target.value;
  if (props.firstNameModifiers?.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1);
  }
  firstName.value = value;
}
</script>

The firstNameModifiers and the emailModifiers are separate props, and each binding’s modifier is applied to its own value.

The combination with the default binding. A component can have the default v-model and the named bindings at the same time. The default uses the modelValue and the update:modelValue, and the named bindings use their own names.

<script setup lang="ts">
const model = defineModel<string>();          // the default
const title = defineModel<string>('title');   // the named
const content = defineModel<string>('content');
</script>

The parent uses the default and the named bindings together:

<MyComponent v-model="value" v-model:title="title" v-model:content="content" />

The object of the bindings. A component with many bindings can group them in a single object and use the v-model with the object. The alternative is the multiple v-model directives. The object approach is the compact alternative, and it is useful when the number of the bindings is large or the fields are dynamic.

<!-- The object prop alternative -->
<script setup lang="ts">
const form = defineModel<{ firstName: string; lastName: string }>({ required: true });
</script>

<template>
  <input v-model="form.firstName" />
  <input v-model="form.lastName" />
</template>

The parent binds the object:

<MyComponent v-model="form" />

The object approach is less explicit than the named bindings, but it is more compact. The choice is a matter of the component’s design.


Complete Example Session

<!-- ============================================
     PART 1: THE MANUAL IMPLEMENTATION
     ============================================ -->

<!-- UserForm.vue -->
<script setup lang="ts">
defineProps<{
  firstName: string;
  lastName: string;
  email: string;
}>();

const emit = defineEmits<{
  'update:firstName': [value: string];
  'update:lastName': [value: string];
  'update:email': [value: string];
}>();
</script>

<template>
  <input
    :value="firstName"
    @input="emit('update:firstName', ($event.target as HTMLInputElement).value)"
  />
  <input
    :value="lastName"
    @input="emit('update:lastName', ($event.target as HTMLInputElement).value)"
  />
  <input
    :value="email"
    @input="emit('update:email', ($event.target as HTMLInputElement).value)"
  />
</template>


<!-- ============================================
     PART 2: THE PARENT USING THE MANUAL IMPLEMENTATION
     ============================================ -->

<script setup>
import { ref } from 'vue';
import UserForm from './UserForm.vue';

const first = ref('');
const last = ref('');
const email = ref('');
</script>

<template>
  <UserForm
    v-model:firstName="first"
    v-model:lastName="last"
    v-model:email="email"
  />
  <p>{{ first }} {{ last }} ({{ email }})</p>
</template>


<!-- ============================================
     PART 3: THE defineModel IMPLEMENTATION
     ============================================ -->

<!-- UserForm.vue -->
<script setup lang="ts">
const firstName = defineModel<string>('firstName');
const lastName = defineModel<string>('lastName');
const email = defineModel<string>('email');
</script>

<template>
  <input v-model="firstName" />
  <input v-model="lastName" />
  <input v-model="email" />
</template>


<!-- ============================================
     PART 4: THE DEFAULT AND THE NAMED TOGETHER
     ============================================ -->

<!-- SearchFilter.vue -->
<script setup lang="ts">
const query = defineModel<string>();              // the default
const category = defineModel<string>('category'); // the named
const sortBy = defineModel<string>('sortBy');
</script>

<template>
  <input v-model="query" placeholder="Search..." />
  <select v-model="category">
    <option value="all">All</option>
    <option value="books">Books</option>
    <option value="electronics">Electronics</option>
  </select>
  <select v-model="sortBy">
    <option value="relevance">Relevance</option>
    <option value="price">Price</option>
  </select>
</template>


<!-- ============================================
     PART 5: THE PARENT USING THE DEFAULT AND THE NAMED
     ============================================ -->

<script setup>
import { ref } from 'vue';
import SearchFilter from './SearchFilter.vue';

const query = ref('');
const category = ref('all');
const sortBy = ref('relevance');
</script>

<template>
  <SearchFilter
    v-model="query"
    v-model:category="category"
    v-model:sortBy="sortBy"
  />
  <p>Search: {{ query }} in {{ category }}, sorted by {{ sortBy }}</p>
</template>


<!-- ============================================
     PART 6: THE WRITABLE COMPUTED
     ============================================ -->

<script setup lang="ts">
import { computed } from 'vue';

const props = defineProps<{
  firstName: string;
  lastName: string;
}>();

const emit = defineEmits<{
  'update:firstName': [value: string];
  'update:lastName': [value: string];
}>();

const firstName = computed({
  get: () => props.firstName,
  set: (value) => emit('update:firstName', value.trim()),
});

const lastName = computed({
  get: () => props.lastName,
  set: (value) => emit('update:lastName', value.trim()),
});
</script>

<template>
  <input v-model="firstName" />
  <input v-model="lastName" />
</template>


<!-- ============================================
     PART 7: THE PER-BINDING MODIFIERS
     ============================================ -->

<script setup lang="ts">
const firstName = defineModel<string>('firstName');

const props = defineProps<{
  firstNameModifiers?: { capitalize?: boolean };
}>();

function handleInput(event: Event) {
  const target = event.target as HTMLInputElement;
  let value = target.value;
  if (props.firstNameModifiers?.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1);
  }
  firstName.value = value;
}
</script>

<template>
  <input :value="firstName" @input="handleInput" />
</template>

<!-- The parent: <UserForm v-model:firstName.capitalize="first" /> -->


<!-- ============================================
     PART 8: THE NAME INPUT COMPONENT
     ============================================ -->

<!-- NameInput.vue -->
<script setup lang="ts">
const firstName = defineModel<string>('firstName', { default: '' });
const lastName = defineModel<string>('lastName', { default: '' });

const emit = defineEmits<{
  submit: [];
}>();
</script>

<template>
  <input v-model="firstName" placeholder="First name" />
  <input v-model="lastName" placeholder="Last name" />
  <button @click="emit('submit')">Submit</button>
</template>

<!-- The parent:
     <NameInput
       v-model:firstName="first"
       v-model:lastName="last"
       @submit="handleSubmit"
     /> -->


<!-- ============================================
     PART 9: THE DATE RANGE PICKER
     ============================================ -->

<!-- DateRangePicker.vue -->
<script setup lang="ts">
const start = defineModel<string>('start');
const end = defineModel<string>('end');
</script>

<template>
  <label>
    Start:
    <input type="date" v-model="start" />
  </label>
  <label>
    End:
    <input type="date" v-model="end" />
  </label>
</template>

<!-- The parent:
     <DateRangePicker v-model:start="startDate" v-model:end="endDate" /> -->


<!-- ============================================
     PART 10: THE COMPLETE SETTINGS COMPONENT
     ============================================ -->

<!-- SettingsPanel.vue -->
<script setup lang="ts">
import { computed } from 'vue';

const theme = defineModel<'light' | 'dark'>('theme', { default: 'light' });
const language = defineModel<string>('language', { default: 'en' });
const notifications = defineModel<boolean>('notifications', { default: true });

const isDark = computed(() => theme.value === 'dark');

function toggleTheme() {
  theme.value = isDark.value ? 'light' : 'dark';
}
</script>

<template>
  <div :class="{ 'settings--dark': isDark }">
    <label>
      Theme:
      <button @click="toggleTheme">{{ theme }}</button>
    </label>
    <label>
      Language:
      <select v-model="language">
        <option value="en">English</option>
        <option value="es">Spanish</option>
        <option value="fr">French</option>
      </select>
    </label>
    <label>
      Notifications:
      <input type="checkbox" v-model="notifications" />
    </label>
  </div>
</template>

<!-- The parent:
     <SettingsPanel
       v-model:theme="theme"
       v-model:language="language"
       v-model:notifications="notifications"
     /> -->

The ten parts show the manual implementation, the parent using it, the defineModel implementation, the default and the named together, the parent using the default and the named, the writable computed, the per-binding modifiers, the name input component, the date range picker, and the complete settings component.


Quick Reference

The Naming Rules

TemplateThe propThe event
v-model="v"modelValueupdate:modelValue
v-model:firstName="v"firstNameupdate:firstName
v-model:title="v"titleupdate:title
v-model:start="v"startupdate:start

The Implementation

ApproachDeclaration
ManualdefineProps + defineEmits
defineModelconst name = defineModel<T>('name')
The defaultconst model = defineModel<T>()
The optionsdefineModel<T>('name', { default, required })

The Modifiers

TemplateThe modifier prop
v-model.trimmodelModifiers
v-model:firstName.trimfirstNameModifiers
v-model:title.capitalizetitleModifiers

The Writable Computed

PartCode
The getget: () => props.name
The setset: (v) => emit('update:name', v)
The templatev-model="name"

The Parent Usage

TemplateEffect
v-model:firstName="first"Binds the firstName
v-model:lastName="last"Binds the lastName
v-model="query"Binds the default
v-model.trim="query"Binds the default with the trim

Best Practices

โœ… Do This:

<script setup>
// Use the defineModel for each binding
const firstName = defineModel<string>('firstName');                       // โœ…
const lastName = defineModel<string>('lastName');                         // โœ…
// Use the default options
const theme = defineModel<'light' | 'dark'>('theme', { default: 'light' }); // โœ…
// Use the writable computed for the transformations
const firstName = computed({ get, set });                                 // โœ…
// Declare the per-binding modifier props
const props = defineProps<{ firstNameModifiers?: { capitalize?: boolean } }>(); // โœ…
// Use the clear names
const start = defineModel<string>('start');                               // โœ…
</script>

โŒ Don’t Do This:

<script setup>
// Don't use the same name for the different bindings
const a = defineModel<string>('name');
const b = defineModel<string>('name'); // conflict                         // โŒ
// Don't forget the emit for the manual implementation
function handleInput(e) { /* no emit */ }                                 // โŒ
// Don't mutate the model ref outside the template handler
firstName.value = 'x'; // OK inside the component                          // โœ…
// Don't use the wrong modifier prop
// v-model:firstName.trim needs firstNameModifiers                        // โŒ
// Don't mix the object prop and the named bindings for the same data
const form = defineModel<{ firstName: string }>('form');
const firstName = defineModel<string>('firstName'); // redundant          // โŒ
</script>

Common Pitfalls

PitfallWhy It HappensFix
The binding does not updateThe wrong event nameUse update:name
The prop is readonlyMutating the propUse the model ref or the emit
The modifier is ignoredNo nameModifiers propDeclare the prop
The name conflictsThe same name for two bindingsUse the distinct names
The defineModel is not availableVue < 3.4Use the manual implementation
The value is not trimmedThe modifier prop not readRead the nameModifiers
The binding is not typedThe wrong genericUse the correct type

Real-World Examples

1. The Name Form

<UserForm v-model:firstName="first" v-model:lastName="last" />

2. The Date Range

<DateRangePicker v-model:start="startDate" v-model:end="endDate" />

3. The Settings Panel

<SettingsPanel
  v-model:theme="theme"
  v-model:language="language"
  v-model:notifications="notifications"
/>

4. The Search Filter

<SearchFilter
  v-model="query"
  v-model:category="category"
  v-model:sortBy="sortBy"
/>

5. The defineModel

const firstName = defineModel<string>('firstName');

6. The Default and the Named

const model = defineModel<string>();
const title = defineModel<string>('title');

7. The Writable Computed

const firstName = computed({ get: () => props.firstName, set: (v) => emit('update:firstName', v.trim()) });

8. The Modifier

<UserForm v-model:firstName.capitalize="first" />

9. The Modifier Prop

defineProps<{ firstNameModifiers?: { capitalize?: boolean } }>();

10. The Complete Component

<NameInput v-model:firstName="first" v-model:lastName="last" @submit="handleSubmit" />

Visual

The Multiple Bindings

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE MULTIPLE BINDINGS                                       โ”‚
โ”‚                                                              โ”‚
โ”‚  Parent:                                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  <UserForm                                           โ”‚    โ”‚
โ”‚  โ”‚    v-model:firstName="first"                         โ”‚    โ”‚
โ”‚  โ”‚    v-model:lastName="last"                           โ”‚    โ”‚
โ”‚  โ”‚    v-model:email="email"                             โ”‚    โ”‚
โ”‚  โ”‚  />                                                  โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ”‚  Expands to:                                            โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  <UserForm                                           โ”‚    โ”‚
โ”‚  โ”‚    :firstName="first"                                โ”‚    โ”‚
โ”‚  โ”‚    @update:firstName="first = $event"                โ”‚    โ”‚
โ”‚  โ”‚    :lastName="last"                                  โ”‚    โ”‚
โ”‚  โ”‚    @update:lastName="last = $event"                  โ”‚    โ”‚
โ”‚  โ”‚    :email="email"                                    โ”‚    โ”‚
โ”‚  โ”‚    @update:email="email = $event"                    โ”‚    โ”‚
โ”‚  โ”‚  />                                                  โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Child:                                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  const firstName = defineModel<string>('firstName'); โ”‚    โ”‚
โ”‚  โ”‚  const lastName = defineModel<string>('lastName');   โ”‚    โ”‚
โ”‚  โ”‚  const email = defineModel<string>('email');         โ”‚    โ”‚
โ”‚  โ”‚                                                       โ”‚    โ”‚
โ”‚  โ”‚  <input v-model="firstName" />                       โ”‚    โ”‚
โ”‚  โ”‚  <input v-model="lastName" />                        โ”‚    โ”‚
โ”‚  โ”‚  <input v-model="email" />                           โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Each binding is independent.                                โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Independence

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE INDEPENDENCE                                            โ”‚
โ”‚                                                              โ”‚
โ”‚  Parent refs:                                                โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  const first = ref('')                               โ”‚    โ”‚
โ”‚  โ”‚  const last = ref('')                                โ”‚    โ”‚
โ”‚  โ”‚  const email = ref('')                               โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  The child updates the firstName:                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  firstName.value = 'Alice'                           โ”‚    โ”‚
โ”‚  โ”‚    โ†’ emit('update:firstName', 'Alice')               โ”‚    โ”‚
โ”‚  โ”‚    โ†’ first.value = 'Alice'                           โ”‚    โ”‚
โ”‚  โ”‚    โ†’ last and email are UNCHANGED                    โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Each binding has its own prop, event, and value.            โ”‚
โ”‚  The bindings do not interfere with each other.              โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Default and the Named

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE DEFAULT AND THE NAMED                                   โ”‚
โ”‚                                                              โ”‚
โ”‚  Parent:                                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  <SearchFilter                                       โ”‚    โ”‚
โ”‚  โ”‚    v-model="query"                                   โ”‚    โ”‚
โ”‚  โ”‚    v-model:category="category"                       โ”‚    โ”‚
โ”‚  โ”‚    v-model:sortBy="sortBy"                           โ”‚    โ”‚
โ”‚  โ”‚  />                                                  โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ”‚  Expands to:                                            โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  <SearchFilter                                       โ”‚    โ”‚
โ”‚  โ”‚    :modelValue="query"                               โ”‚    โ”‚
โ”‚  โ”‚    @update:modelValue="query = $event"               โ”‚    โ”‚
โ”‚  โ”‚    :category="category"                              โ”‚    โ”‚
โ”‚  โ”‚    @update:category="category = $event"              โ”‚    โ”‚
โ”‚  โ”‚    :sortBy="sortBy"                                  โ”‚    โ”‚
โ”‚  โ”‚    @update:sortBy="sortBy = $event"                  โ”‚    โ”‚
โ”‚  โ”‚  />                                                  โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Child:                                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  const query = defineModel<string>();                โ”‚    โ”‚
โ”‚  โ”‚  const category = defineModel<string>('category');   โ”‚    โ”‚
โ”‚  โ”‚  const sortBy = defineModel<string>('sortBy');       โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Per-Binding Modifiers

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THE PER-BINDING MODIFIERS                                   โ”‚
โ”‚                                                              โ”‚
โ”‚  Parent:                                                     โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  <UserForm                                           โ”‚    โ”‚
โ”‚  โ”‚    v-model:firstName.capitalize="first"              โ”‚    โ”‚
โ”‚  โ”‚    v-model:email.trim="email"                        โ”‚    โ”‚
โ”‚  โ”‚  />                                                  โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Child:                                                      โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  const firstName = defineModel<string>('firstName'); โ”‚    โ”‚
โ”‚  โ”‚  const email = defineModel<string>('email');         โ”‚    โ”‚
โ”‚  โ”‚                                                       โ”‚    โ”‚
โ”‚  โ”‚  defineProps<{                                       โ”‚    โ”‚
โ”‚  โ”‚    firstNameModifiers?: { capitalize?: boolean };    โ”‚    โ”‚
โ”‚  โ”‚    emailModifiers?: { trim?: boolean };              โ”‚    โ”‚
โ”‚  โ”‚  }>()                                                โ”‚    โ”‚
โ”‚  โ”‚                                                       โ”‚    โ”‚
โ”‚  โ”‚  // firstNameModifiers = { capitalize: true }        โ”‚    โ”‚
โ”‚  โ”‚  // emailModifiers = { trim: true }                  โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Each binding has its own modifier prop.                     โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Multiple bindingsThe v-model:name for each value
The default bindingmodelValue + update:modelValue
The named bindingname + update:name
The defineModeldefineModel<T>('name')
The options{ default, required }
The modifier propnameModifiers
The writable computedThe get/set for the transformations
The independenceEach binding has its own prop, event, and value
The object alternativeA single defineModel<object>

Key takeaways:

  • A component can have any number of v-model bindings. Each binding is a separate prop and event pair. The default uses the modelValue and the update:modelValue, and the named bindings use the v-model:name syntax with the name prop and the update:name event .
  • The defineModel macro declares each binding with a single call. The defineModel<string>('firstName') returns a ref that is wired to the firstName prop and the update:firstName event. The macro is available in Vue 3.4 and later .
  • The manual implementation uses the defineProps and the defineEmits. The component declares the props and the events with the matching names, and the handlers emit the correct event. The manual approach is the fallback for the Vue versions before 3.4 .
  • The bindings are independent. Each binding has its own prop, event, and value. The child can emit an update for one binding without affecting the others. The parent can bind them to the separate refs .
  • The per-binding modifiers are the nameModifiers props. The v-model:firstName.capitalize populates the firstNameModifiers prop, and the component reads the prop and applies the modifier to that binding only .
  • The writable computed is the pattern for the transformations. The get returns the prop, and the set emits the event. The transformation (the trim, the capitalize) happens in the setter, and the parent receives the transformed value .
  • The defineModel macro accepts the default and the required options. The defineModel<string>('firstName', { default: '' }) provides the default value, and the { required: true } makes the binding required .
  • The object prop is the compact alternative. A component with many bindings can group them in a single object and use the v-model with the object. The object approach is less explicit but more compact .

Remember: A component can have multiple v-model bindings, each with its own name, prop, and event. The default binding uses the modelValue and the update:modelValue, and the named bindings use the v-model:name syntax. The defineModel macro declares each binding with a single call, and the manual defineProps/defineEmits is the fallback. The bindings are independent, the modifiers are per-binding, and the writable computed is the pattern for the transformations. The result is a component that supports the multiple two-way bindings with the explicit prop-and-event contract.



Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!