How do you build a Vue 3 `<DateRangePicker>` that supports `v-model:start` and `v-model:end` on the same component instance?
answer
- v-model takes an argument
- one prop and event per argument
- call the macro twice
- replaced a Vue 2 modifier
basics
~10 sA v-model argument renames the prop and event, so v-model:start binds a start prop and update:start event. In the child, call defineModel('start') and defineModel('end'), or declare both props and both update events.
solid answer
~30 sIn Vue 3 `v-model` accepts an **argument**: `v-model:start="from"` expands to a `start` prop plus an `update:start` listener, and `v-model:end="to"` to an `end` prop plus `update:end`. Because each argument targets its own prop and event, one component can carry several `v-model`s. In the child, `const start = defineModel('start')` and `const end = defineModel('end')` (Vue 3.4+) give two independent refs you bind to two date inputs; before 3.4 you declared `start` and `end` props and emitted `update:start` and `update:end`. Kebab-case arguments such as `v-model:start-date` map to a camelCase `startDate` prop. This replaced Vue 2's `.sync` modifier.
code
vue · 16 lines<!-- DateRangePicker.vue -->
<script setup lang="ts">
const start = defineModel('start', { type: String, required: true })
const end = defineModel('end', { type: String, required: true })
</script>
<template>
<fieldset class="date-range">
<input type="date" v-model="start" :max="end" />
<input type="date" v-model="end" :min="start" />
</fieldset>
</template>
<!-- Parent:
<DateRangePicker v-model:start="from" v-model:end="to" />
passes start and end props and listens for update:start and update:end -->go deeper
Recall that v-model:name binds a prop called name and listens for update:name, and that defineModel('name') handles it in the child.
Explain the argument expansion table, the camelCase mapping for kebab arguments and the pre-3.4 props and emits form.
Weigh two named models against one object model for values with an invariant, and keep the parent as the place that enforces it.
Define how library components with several bindable values name their models so consumers can predict the API across components.
## The problem: one component, two values A date-range picker owns two related values, a start date and an end date. The parent wants to bind both with the same two-way ergonomics it uses for a text input: ```vue <DateRangePicker v-model:start="from" v-model:end="to" /> ``` The default `v-model` can only bind one value, because it always uses the `modelValue` prop. Vue 3 solves this with **`v-model` arguments**. ## How an argument changes the expansion The text after the colon replaces the default names: | Parent writes | Prop passed | Listener added | Modifiers prop | |---|---|---|---| | `v-model="x"` | `modelValue` | `onUpdate:modelValue` | `modelModifiers` | | `v-model:start="from"` | `start` | `onUpdate:start` | `startModifiers` | | `v-model:end="to"` | `end` | `onUpdate:end` | `endModifiers` | Each argument is independent, so a component can accept as many bindings as it declares. The compiler camelizes the event name, so `v-model:start-date` listens for `update:startDate` and passes a prop the child declares as `startDate`. ## Implementing the child with defineModel Since **Vue 3.4**, call the macro once per model: 1. `const start = defineModel('start', { type: String, required: true })` 2. `const end = defineModel('end', { type: String, required: true })` 3. bind each ref to its own `<input type="date" v-model="...">` Each call declares its own prop, modifiers prop and update event. Writing `start.value` emits `update:start` only; the end binding is untouched. Declaring the same name twice is rejected at compile time. ## Implementing it before 3.4 The same contract written out by hand: - `defineProps({ start: String, end: String })` - `defineEmits(['update:start', 'update:end'])` - each input binds `:value` to its prop and emits its own update event on `input` This form still works in Vue 3.5 and is what `defineModel` generates for you. ## Design considerations for a range Two separate models are simple, but a range has an **invariant**: start must not come after end. Options interviewers like to hear: - **Constrain in the UI**: bind `:max="end"` on the start input and `:min="start"` on the end input, so invalid picks are hard to make. - **Validate in the parent**: the parent's variables are the source of truth, so it can reject or swap values in hand-written `@update:start` handlers. - **Use one model holding an object** such as `{ start, end }` when the two values must always change together; then the child must emit a **new object** each time rather than mutating the prop's fields. | Shape | Strength | Weakness | |---|---|---| | two named models | each value binds and updates independently | the invariant spans two events | | one object model | one atomic update for both values | child must copy the object on every change | ## Testing the contract Named models are easy to get subtly wrong, for example emitting `update:modelValue` from a component whose parent binds `v-model:start`. A component test catches that directly: 1. mount the picker with `start` and `end` props and no parent 2. change the start input 3. assert that the component emitted `update:start` with the new date 4. assert that it did **not** emit `update:end` Testing emitted events rather than rendered output keeps the test honest about the contract a parent relies on. It also documents the event names for the next reader, which matters because nothing at compile time checks that a parent's `v-model:start` matches a model the child actually declares; a mismatched argument simply passes an unknown attribute and listens for an event that never comes. ## Naming models Pick argument names that read naturally in the parent template: `v-model:start` and `v-model:end` describe the values, while names like `v-model:value1` leak implementation. Keep them consistent across similar components so that a `<TimeRangePicker>` uses the same pair. ## History In Vue 2 a component had one `v-model` (customisable through the `model` option) and additional two-way props used `:start.sync`. Vue 3 removed `.sync` and the `model` option; `v-model:start` covers both use cases with one syntax.
- What does v-model:start-date on a Vue 3 component expect the child to declare?A `startDate` prop and an `update:startDate` event. The compiler camelizes the listener name, and props passed in kebab case are normalised to camelCase, so `defineModel('startDate')` matches.
- When would you prefer a single object model over two named models for a range?When start and end must always change together, such as picking a preset like last 7 days. One `update:modelValue` with a new `{ start, end }` object is atomic; two named models emit two separate events the parent handles independently.
saying these in an interview costs you the question
- A Vue 3 component can only have one v-model.
- Multiple two-way props on a Vue 3 component need the .sync modifier.
- v-model:start emits update:modelValue with the start value.
- Writing start.value also emits an update for the end binding.
- Both named models share one modifiers object called modelModifiers.