skip to content

How do you implement a custom `.capitalize` modifier for `v-model` on a Vue 3 component, so the parent always receives a capitalised value?

level: middleimportance: nice to knowfreq 32%

answer

  1. modifiers arrive as an object
  2. destructure the macro's result
  3. transform on write, not on read
  4. argument plus Modifiers suffix

basics

~10 s

Modifiers reach the child as an object prop, modelModifiers for the default model. With defineModel, destructure [model, modifiers] and pass a set transformer that capitalises the value when modifiers.capitalize is true.

solid answer

~40 s

When the parent writes `<TitleInput v-model.capitalize="title" />`, Vue passes `modelModifiers: { capitalize: true }` to the child alongside `modelValue`; for a named model such as `v-model:heading.capitalize` the prop is `headingModifiers`. In Vue 3.4+, destructure the macro, `const [model, modifiers] = defineModel({ set(v) { ... } })`, and in the `set` transformer return the capitalised string when `modifiers.capitalize` is true. `set` runs on every write, and its return value is what gets emitted, so the parent always stores the transformed value. A `get` transformer would only change what the child displays, not what the parent stores. Before 3.4 you declared `modelModifiers` with a `() => ({})` default and transformed the value before calling `emit`.

code

vue · 18 lines
vue
<!-- TitleInput.vue -->
<script setup lang="ts">
const [model, modifiers] = defineModel<string>({
  set(value) {
    if (modifiers.capitalize && value) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  },
})
</script>

<template>
  <input type="text" v-model="model" />
</template>

<!-- Parent: <TitleInput v-model.capitalize="title" />
  typing 'hello' stores 'Hello' in title -->

go deeper

for a junior

Recall that modifiers on a component v-model reach the child as an object and that the child implements what they mean.

for a middle

Explain modelModifiers versus <name>Modifiers, the defineModel tuple, and why set rather than get carries the transformation.

for a senior

Handle the edge cases: several modifiers in a set order, unbound use, and the input re-sync when the transformed value does not change.

for a principal

Decide which behaviours deserve a modifier rather than a prop, and require libraries to document supported modifiers.

## What a component modifier is Native `v-model` has built-in modifiers such as `.trim` and `.number`. On a **component**, Vue has no built-in meaning for a modifier; it simply tells the child which modifiers the parent wrote and lets the child decide what they do. That makes custom behaviour such as `.capitalize` possible: ```vue <TitleInput v-model.capitalize="title" /> ``` ## How modifiers reach the child The template compiler turns the modifiers into an **object prop** alongside the model prop: | Parent writes | Model prop | Modifiers prop value | |---|---|---| | `v-model.capitalize="t"` | `modelValue` | `modelModifiers: { capitalize: true }` | | `v-model:heading.capitalize="t"` | `heading` | `headingModifiers: { capitalize: true }` | | `v-model="t"` | `modelValue` | no modifiers prop passed | `defineModel()` declares the matching modifiers prop for you, so nothing else is required on the child side. ## Implementing it with defineModel (3.4+) The returned ref can be **destructured** into the ref and the modifiers object. The options accept two transformers: - **`set(value)`** runs whenever the child writes the ref; its return value is what is emitted to the parent - **`get(value)`** runs whenever the ref is read; it only changes what the child sees For `.capitalize` the rule belongs in `set`, because the goal is that the **parent stores** a capitalised string: 1. `const [model, modifiers] = defineModel({ set(value) { ... } })` 2. inside `set`, return the capitalised value when `modifiers.capitalize` is true 3. otherwise return the value unchanged 4. bind `model` to the inner `<input v-model="model">` The `set` function reads `modifiers` lazily, at write time, so referencing the destructured variable inside it is safe even though it is defined in the same statement. ## Implementing it before 3.4 The explicit form is still valid: - declare `modelValue` and `modelModifiers: { default: () => ({}) }` with `defineProps` - declare `update:modelValue` with `defineEmits` - in the input handler, transform the value when `props.modelModifiers.capitalize` is true, then emit it The empty-object default matters: without it, reading `props.modelModifiers.capitalize` throws when the parent used no modifiers. ## Edge cases worth naming - **The input and the stored value.** The user types `hello`; the child emits `Hello`; the parent stores it and passes it back, so the input shows `Hello`. If a later keystroke produces a transformed value equal to the one already emitted, the parent sees no change. Vue 3.5 detects that case and forces a local update so the input still re-syncs. - **Unbound use.** If the component is used without any `v-model`, there are no modifiers and the ref keeps its value locally; the transformer still decides what is emitted. - **Several modifiers.** The object can hold several keys, for example `{ capitalize: true, trim: true }`; apply them in a deliberate order inside `set`. ## Modifier, prop or parent handler A custom modifier is one of three ways to get the same transformation, and interviewers like to hear why you picked it: | Option | Parent writes | Good when | |---|---|---| | custom modifier | `v-model.capitalize="t"` | the behaviour is an on or off flag tied to this binding | | ordinary prop | `v-model="t" capitalize` | the behaviour also affects rendering, or needs a value such as a locale | | parent handler | `@update:modelValue="v => (t = cap(v))"` | only one parent needs it, so the child stays generic | Modifiers are limited to boolean flags: the object only records which ones were present. As soon as the transformation needs configuration, such as a list of words to keep lowercase, a real prop is the better API. Modifiers also belong to one binding, so on a component with several named models each model has its own `<name>Modifiers` object and the flags do not leak between them. ## Why transform in the child at all A modifier keeps the parent's template declarative: consumers opt into a behaviour with one word instead of writing an `@update:modelValue` handler. The trade-off is discoverability, since a custom modifier is invisible unless documented, so library components should list supported modifiers next to their props.

  • Why put the capitalisation in set rather than get when using defineModel?
    `set` changes the value that is emitted, so the parent stores the capitalised string. `get` only changes what the child reads, leaving the parent's variable uncapitalised, which breaks anything else that reads it.
  • Before Vue 3.4, why did modelModifiers need a default of an empty object?
    Without a modifier on the parent's v-model, the prop is not passed. Reading `props.modelModifiers.capitalize` would then throw on `undefined`; the `() => ({})` default makes the check safe.

saying these in an interview costs you the question

  • Vue applies .capitalize automatically, like the built-in .trim modifier.
  • Custom modifiers are passed as separate boolean props named after each modifier.
  • A get transformer changes the value the parent stores.
  • A named model's modifiers arrive in modelModifiers too.
  • Custom v-model modifiers require a directive to be registered.