DEV Community

Cover image for Authoring Validatable Vue Input Components
Abdelrahman Awad
Abdelrahman Awad

Posted on • Originally published at Medium on

Authoring Validatable Vue Input Components

Building Vue.js components is very fun and straightforward, you can build almost anything and it will work most of the time. But when it comes to components that serve as inputs there are few things to cover.

We will start with covering the basics using Vue.js guide then build our way up to integrate the component to work seamlessly with vee-validate.

Implement the v-model interface

As mentioned in the official Vue guide, your component should be able to work with the v-model directive, which boils down to emitting an input event whenever the value changes and it should accept a value input, which of course can be customized to cover other events and props.

As a start, our custom text component would look like this:

<template>
  <input type="text" :value="value" @input="onInput" />
</template>

<script>
export default {
  props: {
    value: String
  },
  methods: {
    onInput (e) {
      this.$emit('input', $event.target.value);
    }
  }
};
</script>

It doesn’t do much yet, it just accepts a value prop and emits an event whenever the user manipulates the input via the native input event, so in a way, it translates the interaction from native HTML input to a Vue component.

This way, using v-model is the only way this component mutates the state, which is simple and manageable. And above all else is re-usable.

Add events as needed

Native HTML inputs have a plethora of events that you can listen to, but most of the time you are only interested in a few of them, in our case we have been only interested in the input event, we should add some useful events as well, like focus, blur, and change.

<template>
  <input
    type="text"
    :value="value"
    @input="updateValue"
    @change="updateValue"
    @blur="$emit('blur')"
    @focus="$emit('focus')"/>
</template>

<script>
export default {
  props: {
    value: String
  },
  methods: {
    updateValue (e) {
      this.$emit('input', $event.target.value);
    }
  }
};
</script>

This might not be necessary when working with your own components in your own projects but if you are planning to publish your components to the masses then you might need to cover a few events that the users might be interested in listing to, for example, blur could be used to trigger UI change or trigger validation on that input.

Add props

Like events, some users may be interested in setting some props on your component like they would on a native HTML input, for example, the disabled attribute is extremely valuable, maybe a label and name props as well.

Also having the input type always set to text isn’t very accessible on mobile devices since the keyboards can be context-aware to display relevant characters to the input type, like @ for emails and .com for URLs.

Let us add our props and bind it to the inner input attributes:

<template>
  <div class="form__input">
    <label v-if="label" class="form__label">{{ label }}</label>
    <input
      class="input"
      :type="type" 
      :value="value" 
      :disabled="disabled" 
      @input="updateValue" 
      @change="updateValue" 
      @blur="$emit('blur')" 
      @focus="$emit('focus')"
    />
  </div>
</template>

<script>
export default {
  props: {
    name: {
      type: String
    },
    label: {
      type: String
    },
    disabled: {
      type: Boolean,
      default: false
    },
    value: String,
    type: {
      type: String,
      default: 'text',
      validate: (val) => {
        // we only cover these types in our input components.
        return ['text', 'url', 'email', 'password', 'search'].indexOf(val) !== -1;
      }
    }
  },
  methods: {
    updateValue (e) {
      this.$emit('input', $event.target.value);
    }
  }
};
</script>

We can go a long way with this, for example: autocomplete , placeholder and plenty more .

Enable Validation

Most likely you want to validate this little component of ours, we have two approaches:

  • Validate it from the parent as a response to the input event (reactive).
  • Have it validate itself whenever the value changes (passive).

Now while the second option makes it sound more contained and useful, you should realize that it forces validation on the component, It is opt-out instead of opt-in.

Furthermore in the lines of the single responsibility pattern: Should the component be concerned if its value is valid? or is it the concern of the consumer of said value, the consumer being the parent component or the form component.

The first approach simplifies our component implementation and makes it lighter, the component should only report its value, not its validness.

Vee-validate supports both approaches, but depending on your implementation, the second approach could be more difficult to maintain since it requires communicating the errors to the parent component to prevent form submission for example until everything is valid.

For now, let us go with the first approach, we don’t have to add anything to our component, it will just work:


<template>
  <div>
    <text-input 
      name="email" 
      label="Email Address" 
      v-validate="'required|email'"
      v-model="email"
      type="email"
    ></text-input>
  </div>
</template>

<script>
import TextInput from '@components/inputs/Text';
export default {
  components: {
    TextInput
  },
  data: () => ({
    email: null
  })
};
</script>

Here comes the issue, our parent component can tell if the component is valid or not using the validator API, but not our user. Going back to our question if the component should be concerned if its value is valid or not, maybe it should since we need to display some red flags to the user as well.

But instead of it saying “my value is valid” maybe it should instead ask: “is my value valid?” which can be done using props easily.

Let us add error prop to our component and bind it to its view:

<template>
  <div class="form__input">
    <label v-if="label" class="form__label">{{ label }}</label>
    <input
      class="input"
      :type="type" 
      :value="value" 
      :disabled="disabled" 
      @input="updateValue" 
      @change="updateValue" 
      @blur="$emit('blur')" 
      @focus="$emit('focus')"
    />
    <p v-if="error" class="form__error">{{ error }}</p>
  </div>
</template>

<script>
export default {
  props: {
    error: {
      type: String
    },
    // ...
  },
  methods: {
    // ...
  }
};
</script>
<template>
  <div>
    <text-input 
      name="email" 
      label="Email Address" 
      v-validate="'required|email'"
      v-model="email"
      type="email"
      :error="errors.first('email')"
    ></text-input>
  </div>
</template>

<script>
import TextInput from '@components/inputs/Text';
export default {
  // ...
};
</script>

Well, now we got a clean component that can be validated with a few nice features. The only thing remaining is to fix some issue with vee-validate.

Vee-validate validates components the same way it validates HTML inputs, mainly using events but what happens when you call validateAll to validate manually before form submission. vee-validate doesn’t know the current value of the input since it doesn’t track them, also what if you want to validate on blur instead? Our component does not emit the value with the blur event, so the validator wouldn’t know what to do.

VeeValidate Component Options

There is a newly added feature called component options, which is a contract between the component and VeeValidate. It allows your component to tell vee-validate some stuff, like how to fetch the current value, the component name and which events should be used to validate the component.

<script>
export default {
  $_veeValidate: {
    // must not be arrow functions.
    // the name getter
    name () {
      return this.label;
    },
    // the value getter
    value () {
      return this.value; // `this` is the component instance, `value` is the prop we added.
    }
  },
  props: {
    // ...
  },
  methods: {
    // ...
  }
};
</script>

And that is it, we got ourselves a nice custom input component that plays well with vee-validate. Of course, there is room for improvements and there are already plenty of Vue.js component frameworks that do this very well like Vuetify.

What about other input types?

Text inputs while versatile, are lacking in many ways for some input value types. Like dates, choices, files and whatever you can think of.

I have included in this sandbox other types of inputs that you might find useful for your next component implementation. They share the same principle of what we did here: Passing values to the model and telling the validator how to fetch it.

We covered a lot here! Now go on and give your input components a nice touch. thanks for reading!

Top comments (0)