Adding v-model Support to Custom Vue.js Components

Joshua Bemenderfer

While v-model is a powerful asset for adding two-way data-binding to vanilla components, it’s not always readily apparent how to add support for v-model to your own custom components. Turns out though, it’s pretty simple.

Introduction

To understand how to implement v-model support in your components, you need to understand how it works under the hood. Even though it seems like magic, v-model=”syncedProp” is actually a very simple shorthand for :value=”syncedProp” @:input=”syncedProp = arguments[0]” (or :value=”syncedProp” @:input=”syncedProp = $event.target.value” in vanilla components.)

As such, all your component needs to do to be compatible with v-model is accept a :value property and emit an @input event when the user changes the value.

Basic Implementation

Say you have a date picker component that accepts a month and a year value in an object with the form: {month: 1, year: 2017}. You want that component to have two inputs, one for the month and one for the year, and update the bound object through v-model. Here’s how you would implement that.

DatePicker.vue

<template>
  <div class="date-picker">
    Month: <input type="number" ref="monthPicker" :value="value.month" @input="updateDate()"/>
    Year: <input type="number" ref="yearPicker" :value="value.year" @input="updateDate()"/>
  </div>
</template>

<script>
export default {
  props: ['value'],

  methods: {
    updateDate() {
      this.$emit('input', {
        month: +this.$refs.monthPicker.value,
        year: +this.$refs.yearPicker.value
      })
    }
  }
};
</script>

And here’s how another component would use the date picker:

WrapperComponent.vue

<template>
  <div class="wrapper">
    <date-picker v-model="date"></date-picker>
    <p>
      Month: {{date.month}}
      Year: {{date.year}}
    </p>
  </div>
</template>

<script>
import DatePicker from './DatePicker.vue';

export default {
  components: {
    DatePicker
  },

  data: {
  	date: {
      month: 1,
      year: 2017
    }
  }
})
</script>

As you can see, it simply takes a :value property and emits an @input event with the updated date. Nothing too complicated at all!

Advanced Usage

By using one or more computed properties, we can denormalize input data such as strings into a format that the input elements can more easily work with. This is often used with more advanced custom components that have to deal with a wide variety of potential input formats, such as color pickers.

For our basic date picker example, let’s assume that the format the date is passed in is now a string with the structure m/yyyy. By using a computed property (in this case, splitDate), we can split the input string into an object with month and year properties, while making only minimal modifications to the date picker component.

StringyDatePicker.vue

<template>
  <div class="date-picker">
    Month: <input type="number" ref="monthPicker" :value="splitDate.month" @input="updateDate()"/>
    Year: <input type="number" ref="yearPicker" :value="splitDate.year" @input="updateDate()"/>
  </div>
</template>

<script>
export default {
  props: ['value'],

  computed: {
    splitDate() {
      const splitValueString = this.value.split('/');

       return {
        month: splitValueString[0],
        year: splitValueString[1]
      }
    }
  },

  methods: {
    updateDate() {
      const monthValue = this.$refs.monthPicker.value;
      const yearValue = this.$refs.yearPicker.value;
      this.$emit('input', `${monthValue}/${yearValue}`);
    }
  }
};
</script>

Note: This is an utterly terrible way of handling date input and is only used to illustrate the points in this article. Don't use it in anything you hold dear.

Once you get familiar with the concepts presented here, you’ll probably find yourself using v-model for any and every component you create that takes user input. It’s a remarkably simple but powerful way of adding two-way data binding support in your own custom components.

✖ Clear

🕵 Search Results

🔎 Searching...