All you need to do is to use the v-model directive. You may also have learned that you can use this directive with any custom component since v-model is just a syntax sugar to cover both ways of the data binding. You can learn more about this here. Hence

<input v-model="searchText">

turns into

<input
  v-bind:value="searchText"
  v-on:input="searchText = $event.target.value"
>

As you can see, in order to implement the support, you have to declare a prop variable called value and emit an event labeled input. And that’s it.

However, you will quickly find out that at this point the component indeed supports the v-model directive, but it doesn’t work at all without it. That’s often undesirable. For instance, imagine you’d like to create a custom search component that includes a text input. Since it’s a mere extension of a text input, it’s reasonable that it should support v-model. But it is also reasonable that you’d like to be able to use it without it since the input inside would normally work straight away had it been a plain HTML element. Let’s tackle this.

Optional v-model support 🔗

Let’s start by creating a simple search component that will accept value as prop. If the user doesn’t provide it, it’s initiated to an empty value.

  props: {
    value: {
      type: String,
      default: "",
    },
  },

However, we can’t use this prop directly in the input since that would mutate it which is not recommended. To circumvent this problem we’ll create a clever computed value that will use the value prop if passed from the parent, or a custom local value otherwise. We’ll make use of the extended computed property syntax where one can declare different functions for setter and getter of the computed function.

  data() {
    return {
      localValue: this.value,
    };
  },
  computed: {
    searchValue: {
      get() {
        return this.isValuePropSet() ? this.value : this.localValue;
      },
      set(value) {
        this.$emit("input", value);
        this.localValue = value;
      },
    },
  },
  methods: {
    isValuePropSet() {
      return (
        !!this.$options.propsData && this.$options.propsData.value !== undefined
      );
    },
  },

Let’s first take a look at the getter. When retrieving the value, the isValuePropSet() method is invoked. This method returns true when the value prop was set by the parent, not initialized to empty string by the default property. So when it was set from the outside, we’ll just return the value property and the component works as if it was implemented as a regular component with v-model support. However, when the value was not set, then the getter returns localValue instead. In the setter the current value is both emitted as an input event and stored in the localValue.

With this pattern, we can bind the clever searchValue computed property to the input as usual

<input v-model="searchValue" />

And that’s it. The search component works with v-model attached as well as without it. Check out the example sandbox to see it wholly in action.