skip to content

How do you build a Vue 3 `<DateRangePicker>` that supports `v-model:start` and `v-model:end` on the same component instance?

level: middleimportance: should knowfreq 50%

answer

  1. v-model takes an argument
  2. one prop and event per argument
  3. call the macro twice
  4. replaced a Vue 2 modifier

basics

~10 s

A 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 s

In 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
vue
<!-- 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

for a junior

Recall that v-model:name binds a prop called name and listens for update:name, and that defineModel('name') handles it in the child.

for a middle

Explain the argument expansion table, the camelCase mapping for kebab arguments and the pre-3.4 props and emits form.

for a senior

Weigh two named models against one object model for values with an invariant, and keep the parent as the place that enforces it.

for a principal

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.