Skip to the solution
MediumFrontend8 min read

How does v-model work on components?

899 views

The problem

Question presented to candidate: "We are building a design-system SearchBox and a NameFields component that edits a first and last name. Parents should be able to write v-model on them like on a native input. What contract does the component have to implement, and how do you support two models on one component?"

What a strong answer should cover:

  • On a component, v-model="x" expands to :modelValue="x" plus @update:modelValue="v => x = v": a prop down, an event up.
  • Named models: v-model:title="t" uses a title prop and an update:title event, which is how one component supports several models.
  • The child must never mutate the prop; it emits the new value and the parent assigns it (one-way data flow).
  • Forwarding to an inner <input>: a writable computed (get reads the prop, set emits) or, since 3.4, defineModel().
  • Vue 2 differences: the default was value + input, and .sync is replaced by v-model:arg.
  • Modifiers travel as a modelModifiers prop.

Clarifying questions expected:

  • "Which Vue version is the design system on? From 3.4 I would use defineModel."
  • "Does the parent always bind the value, or should the component also work uncontrolled?"

Code / implementation expected: Yes. A component implementing the prop + emit contract, ideally with a named second model.

  • v-model

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

8 min read Β· 9 sections

Read the code

VueThe v-model contract by hand: default and named models
<script setup>
import { ref, defineComponent, h } from "vue";

// The v-model contract, written by hand:
//   accept a prop called modelValue, emit "update:modelValue" with the new value
const SearchBox = defineComponent({
  props: { modelValue: { type: String, default: "" } },
  emits: ["update:modelValue"],
  setup(props, { emit }) {
    return () =>
      h("input", {
        class: "search",
        value: props.modelValue,
        onInput: (e) => emit("update:modelValue", e.target.value),
      });
  },
});

// Named models: v-model:first and v-model:last -> props first/last, events update:first/update:last
const NameFields = defineComponent({
  props: { first: String, last: String },
  emits: ["update:first", "update:last"],
  setup(props, { emit }) {
    return () => [
      h("input", { class: "first", value: props.first, onInput: (e) => emit("update:first", e.target.value) }),
      h("input", { class: "last", value: props.last, onInput: (e) => emit("update:last", e.target.value) }),
    ];
  },
});

const query = ref("vue");
const first = ref("Ada");
const last = ref("Lovelace");
const events = ref([]);
</script>

<template>
  <!-- sugar for :modelValue="query" @update:modelValue="v => query = v" -->
  <SearchBox v-model="query" />

  <!-- two independent models on one component -->
  <NameFields v-model:first="first" v-model:last="last" />

  <!-- the long form still works, and lets you watch the event go by -->
  <SearchBox
    class="long"
    :modelValue="query"
    @update:modelValue="(v) => { events.push(v); query = v }"
  />

  <p class="out">query={{ query }} | name={{ first }} {{ last }} | long-form events={{ events.join(", ") }}</p>
</template>
VueSearchBox.vue: forwarding v-model to an inner input with a writable computed
<script setup>
import { computed } from "vue";

const props = defineProps({ modelValue: { type: String, default: "" } });
const emit = defineEmits(["update:modelValue"]);

// A writable computed: reads the prop, writes by emitting.
// Now an inner native v-model can use it directly.
const value = computed({
  get: () => props.modelValue,
  set: (v) => emit("update:modelValue", v),
});
</script>

<template>
  <input class="search" v-model="value" placeholder="Search" />
</template>

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 34 of 92 in Vue.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Vue.js questions
Up nextMedium

Can a computed property be writable?

Continue