Looking for a reliable and modern front-end technology for your project? Learn how Vue.js will help you release your product faster.

Rethinking validations for Vue.js

Damian Dulisz

November 29, 2016 | plugin vue vue.js vuejs frontend validation validators open-source

The popular approach

There are already good validator libraries dedicated for Vue.js like vue-validator or vee-validate.

Both those libraries work quite similarly in that they require you to write the rules inside the template. Often this is all you actually need for your application. Let’s take a look at this example from vee-validate:

<input v-model="email" v-validate data-rules="required|email" :class="{'input': true, 'is-danger': errors.has('email') }" type="text" placeholder="Email">

The problem

However, if you work on a more data oriented application you’ll notice that you’re validating much more than just your inputs. Then you have to combine the above approach with custom validation methods or computed values. This introduces additional noise both in the templates and in your code. I also believe that templates are not the best place for declaring application logic.

This becomes even more visible when you want to validate collections or values combined from different sources like Vuex getters, user inputs and computed values. Or when your validation rules depend on other validation results etc.

Introducing Vuelidate

What I personally love about Vue.js is that it structures your code really well: data, methods, computed values, watchers, Vuex, routing – everything has its place. So why shouldn’t we create one for validations?

To meet this need, together with my colleague Paweł Grabarz (@frizi09), we’ve created vuelidate – a different approach to deal with data validation in Vue.js.

Validations beyond forms

The biggest difference you will notice is that the validations are completely decoupled from the template. It means that instead of providing rules for different inputs inside a template, you declare those rules for your data model. This is similar to how Ember does it.

It also works for Vuex getters, the route object and computed values. Hence, you can go even deeper and validate other validations results, just for the sake of doing it.

The above example could be recreated like this:

import Vue from 'vue'
import Validations from 'vuelidate'
import { email, required } from 'vuelidate/lib/validators'

Vue.use(Validations)

new Vue({
  el: '#app',
  data () {
    return {
      email: '',
    }
  },
  validations: {
    email: { required, email } // rules object
    }
}
<input v-model="email" :class="{'input': true, 'is-danger': $v.email.$invalid }" type="text" placeholder="Email">

The validation results object is available at this.$v. For the above example, it would look like this:

$v: {
    email: {
      $dirty: false,
      $error: false,
      $invalid: true,
      required: false,
    email: false
    },
    $dirty: false,
    $error: false,
    $invalid: true,
}

Sure, at the beginning the whole thing might seem a bit verbose and not as elegant as adding the rules inside the template. However, the moment you need something more complex, you will probably fall in love with this approach.

Custom validators

Providing your custom validators is as easy as passing a function into the rules object. Want to use Moment.js queries like isBefore or isAfter? No problem, it’s that easy:

validations: {
  startDate: {
    // given date is a moment object
    isBefore (date) { return date.isBefore(this.endDate) }
  },
  endDate: {
    isAfter (date) { return date.isAfter(this.startDate) }
  }
}

Enjoying lodash and its function composition methods _.pipe and _.compose? We got you covered.

Look at this example, where we coerce a freehand string to a moment object and then validate it.

validations: {
  startDate: {
    isBefore (date) {
        return _.pipe(
          moment, // coerce to a moment object
          momentDate => momentDate.isBefore(this.endDate) // validate it!
        )(date)
    }
  }
}

You can easily handle much more complicated situations like comparing date ranges using moment-range or building dynamic validators.

Usage and reusability

Because we don’t include any validators inside the core itself, the bundle is much smaller compared to other validator plugins. Of course we ship with some ready to use validators, but they are stored as separate files.

The currently available validators and helpers include:

  • alpha
  • alphaNum
  • and
  • between
  • maxLength
  • minLength
  • or
  • required
  • sameAs

This list will grow as it doesn’t really affect the validator size, because you can only use the ones you actually need.

You can import them like this:

import require from 'vuelidate/lib/validators/require'
import between from 'vuelidate/lib/validators/between'

Or import them from validators/index.js file.

import { require, between } from 'vuelidate/lib/validators'

Thanks to tree-shaking in Rollup.js or Webpack 2, both of the above will result in the same bundle size.

Assuming that you use one of the above module bundlers (and I think you should!), if you really want to cut down the size of the initial common bundle file, you don’t even have to install the plugin at all.

So instead of:

// main.js
import Vue from 'vue'
import Validations from 'vuelidate'

Vue.use(Validations)

You can just mix the validation functionality into the components that actually make use of it.

// formView.vue
import { validationsMixin } from 'vuelidate'

export default {
  mixins: [validationsMixin]
}

As for reusability – you probably already guessed it. Just like the plugin itself, the validation rules can be moved into mixins and be shared across different parts of your application. Or just stored inside an object and imported where needed. Or built with closures. Just make sure that the validator function returns a truthy value when the value passes and a falsy value when it doesn’t.

To make it easier for you, we make sure to call each rule with the component’s this context, similar to how Vue’s methods and computed values work.

Validations groups and collections

When handling larger or more complex forms it might be convenient to put the validations inside groups.

The code could look like this:

validations: {
  name: { alpha },
  email: { required, email }
  users: {
    minLength: minLength(2)
  },
  tags: {
    maxLength: maxLength(5)
  },
  formA: ['name', 'email'],
  formB: ['users', 'tags']
}

That’s it. :)

The validation object for the $v.fromA group would look like this:

formA: {
  $dirty: false,
  $error: false,
  $invalid: true,
  name: {
    $dirty: false,
    $error: false,
    $invalid: true,
    minLength: false
  },
  name: {
    $dirty: false,
    $error: false,
    $invalid: true,
    required: false,
    email: false
  }
}

As I mentioned earlier – we also support collections (both arrays and objects). For this we introduced a special $each key that lets you define validators for each element/key.

Take a look at this example:

validations: {
  users: {
    minLength: minLength(1), // the users array must include at least 1 user
    $each: {
      email: { // Each user must have their email field set correctly
        required,
        email
      }
    }
  }
}

Getting back to form inputs

Okay, that’s cool, but what about the useful form indicators like $dirty?

That’s a good question!

Because we decided not to use any custom directives (at least yet), you actually have to handle this mostly by yourself. Mostly, because we expose some handy functions for doing this.

<input
  v-model="email"
  :class="{'input': true, 'is-danger': $v.email.$error }"
  @change="$v.email.$touch()"
  type="text"
  placeholder="Email">

It might not look as elegant as in other validators, but I believe this is a price we can pay for the control we gain. On the other hand – if you use Vuex, it is trivial to incorporate the above inside the update method.

<input
  :value="email"
  :class="{'input': true, 'is-danger': $v.email.$error }"
  @change="update('email', $event)"
  type="text"
  placeholder="Email">
methods: {
  update (field, e) {
    this.$v[field].$touch()
    dispatch('updateField', field, e.target.value.trim())
  }
}

Depending on when you trigger the $touch() method, the $dirty flag can be used both as a touched and dirty equivalent, depending what you actually need.

To reset the validation dirty flag call the $reset() method:

this.$v.$reset()
// or for a specific value
this.$v.email.$reset()

Both $touch() and $reset() traverse down the object tree. This means that if you $reset() a group or nested model, it automatically cleans the $dirty flag in each member of the group, but not the other way around. The same goes for $touch() method.

Summary

I hope this introduction gives you a better insight on the approach we’re taking with vuelidate. The project is still in its early days, hence the core is just ~230 lines of code.

Check out the documentation for more code samples and a getting-started guide!

As always, feel free to share your feedback in the comments section! :)

Thanks for reading!

 

Sign up!

Learn more!
Learn more!