How to make reusable form input element in Vue.js 2.6 and Vue.js 3.0

Some months ago I’ve attended the Vue.js Workshop held by Chris Fritz. In this workshop, he showed us a Vue.js component wrapping technique which is called transparent wrapper component.

With this technique, you can give to native components like input, selects and other inputs superpowers. We will go over every part of this pattern and understand how it’s done.

Let’s start with a simple input component first.

Basic Input

In the beginning, we will create <CustomInput/> component
// CustomInput.vue

<template>
  <input v-model="name">
</template>

<script>
export default {
  data() {
    return {
      name: ""
    }
  }
}
</script>

Making v-model work on the parent

The first problem is that we want to make v-model work on these components so everyone can use it with the same syntax we already know. We know that v-model is just a shorthand for binding a value and emitting an event.
So let’s do this!
// CustomInput.vue
<template>
  <input 
    type="text" 
    :value="value" 
    @input="$emit('update', $event.target.value)"
  />
</template>

<script>
export default {
  props: ["value"],
  model: {
    prop: "value",
    event: "update"
  }
};
</script>

// App.vue

<CustomInput v-model="name" />

How to use the native event on custom components

I see people adding custom events for each native event to the component, but we can do the same by binding the parent event listeners to our child component with v-on="$listeners".
// CustomInput.vue
<template>
  <input 
    type="text" 
    v-on="$listeners" 
    :value="value" 
    @input="$emit('update', $event.target.value)"
  />
</template>

<script>
export default {
  props: ["value"],
  model: {
    prop: "value",
    event: "update"
  }
};
</script>

// App.vue
<CustomInput v-model="name" @keydown.enter="search" />

How to disable automatic attribute inheritance in Vue 2.x

By default, all HTML attributes of the element, which are not declared as props are automatically inherited by the first wrapping element in your component. Only :styles and :classes are converted to props automatically and they are not passed down by attribute inheritance. This behaviour is a little bit opaque and many people don’t understand it at first.
// CustomInput.vue
<template>
  <input 
    type="text" 
    v-on="$listeners" 
    :value="value" 
    @input="$emit('update', $event.target.value)"
  />
</template>

  <script>
  export default {
    props: ["value"],
    model: {
      prop: "value",
      event: "update"
    }
  };
  </script>


// App.vue
  <CustomInput
    placeholder="Your Name"
    @keydown.enter="search"
    v-model="name"
    label="Name"
 />

So if you are using input element as the only element in your custom input component everything will work just fine. But if we start to introduce some div or label as a wrapper for our input, automatic attribute inheritance will add a placeholder to the div element and not to the desired input element.

// CustomInput.vue
  <template>
    <div>
      <!-- placeholder is not longer on the `input` element -->
      <input 
        type="text" 
        v-on="$listeners" 
        :value="value" 
        @input="$emit('update', $event.target.value)">
    </div>
  </template>

  <script>
  export default {
    props: ["value"],
    model: {
      prop: "value",
      event: "update"
    }
  };
  </script>

// App.vue
  <CustomInput
    placeholder="Your Name"
    @keydown.enter="search"
    v-model="name"
    label="Awesome Label"
  />

We can fix this by disabling the automatic attribute inheritance and add it to the element where it belongs to.

https://vuejs.org/v2/api/#inheritAttrs
https://vuejs.org/v2/api/#vm-attrs

// CustomInput.vue
  <template>
    <label>
      <div>{{label}}</div>
      <input 
        v-bind="$attrs" 
        type="text" 
        v-on="$listeners" 
        :value="value" 
        @input="$emit('update', $event.target.value)">
    </label>
  </template>

  <script>
  export default {
    inheritAttrs: false,
    props: ["value", "label"],
    model: {
      prop: "value",
      event: "update"
    }
  };
  </script>

// App.vue
  <CustomInput
    placeholder="Your Name"
    @keydown.enter="search"
    v-model="name"
    label="Name"
  />

Property Validation

Our basic input should be able to handle the following types: text, password, email, number, url, tel, search and color. In the next step, we will create a prop validation.

// CustomInput.vue
<script>
const TYPES = [
  'text', 
  'password', 
  'email', 
  'number', 
  'url', 
  'tel', 
  'search',  
  'color'
]
const includes = types 
  => type 
  => types.includes(type)

export default {
  inheritAttrs: false,
  props: {
    label: {
      type: String,
      default: ''
    }
    value: {
      type: [String, Number],
      default: ''
    },
    type: {
      type: String,
      default: 'text',
      validator (value) {
        const isValid = includes(TYPES)(value)
        if (!isValid) {
          console.warn(`allowed types are ${TYPES}`);
        }
        return isValid
      }
    }
  },
  model: {
    prop: "value",
    event: "update"
  }
};
</script>

Vue 3.0 v-model, how it might be in the future

You can watch the Video where Chris explains how Transparent Wrapper Component might change in the Vue 3.0

 

Transparent Wrapper Component Pattern works well for all kind of inputs, expect multiple select.
Checkboxes can be tricky too since the native v-model emits a String when you have a single element and an array when you have multiple selections.
Since v-model is a directive it also supports the directive modifiers: https://vuejs.org/v2/guide/forms.html#Modifiers

 

To achieve the same results with you custom input component, you’ll need to implement this behaviour yourself.
Once all components are made, you can reuse them in all of your apps.

Demo

6 Thoughts to How to make reusable form input element in Vue.js 2.6 and Vue.js 3.0

  • Jeremy Lowery
    Jeremy Lowery

    Thank you for this article. It’s a wonderful reference!

    02.09.2021 01:11
  • nomo
    nomo

    What’s the model attribute for? Why is it present?

    09.12.2020 22:25
    • Aleksej Dix
      Aleksej Dix

      You can control how the v-model behaves. What property it will receive and what even it will emit

      11.12.2020 14:35
  • Luke Rocco
    Luke Rocco

    Explained very well. Thanks for this article 👍

    28.11.2020 20:10
  • Gillian
    Gillian

    Fantastic article! Do you have an example for ?

    25.10.2020 22:20
    • Aleksej Dix
      Aleksej Dix

      Sure you can read it now on the official Vue3 documentation

      01.12.2020 19:28

Leave a Reply