Shawn Wildermuth

Author, Teacher, and Filmmaker
.NET Foundation Board Member

Playing with Vuelidate Alpha with Vue 3

Playing with Vuelidate Alpha with Vue 3

Vuelidate is reworking their project in light of Vue 3's release. It is only in Alpha, but I like the approach and integrated it into my Microservice demo I'm working on.

In light of this, I thought I'd talk about how Vuelidate is now working with the Composition API. While it is still early in development, I love the approach.

For this example, i'll point you to my Payment view in my Microservice project (a work in progress):

Checkout.vue

Let's set it up. First you'll need to install the packages. The new Vuelidate has updated to separate their core and validators packages, so you'll need both:

> npm i @vuelidate/core @vuelidate/validators --save

Note this does work with Vue 2 and the Composition API too if you're rocking that version.

With that installed, You can still install it as a plugin, but I am not sure it's actually necessary as with the Composition API, it's just composable. But in case, you do, just do this:

// Vue 3
//...
import { VuelidatePlugin } from "@vuelidate/core";

const app = createApp(App);

app.use(store)
   .use(router)
   .use(VuelidatePlugin)
   .mount('#app');

But the real magic works in the component itself. First, let's look at the Interface I want to provide validation for:

export default interface Payment {
  creditCard: string;
  expirationMonth: number;
  expirationYear: number;
  postalCode: string;
  validationCode: string;
}

A few thing I need to get right in validating a payment. To do this, I can just create a rule-set (I'm doing it directly in the Vue but that's not necessary):

setup(props, ctx) {
  const payment = reactive({} as Payment);

  const rules = {
    creditCard: { required, creditcard },
    expirationMonth: { required, numeric, length: length(2) },
    expirationYear: { required, numeric,  length: length(2) },
    postalCode: { required, minLength: minLength(5) },
    validationCode: { required, numeric },
  };

  //..
},

Notice that the rules are just an object with the validators assigned to each property. The validations (required, number, and minLength) are standard validators provided by Vuelidate:

import { required, numeric, minLength } from "@vuelidate/validators";

The creditcard and length validation rules are custom ones I wrote. I'll explain how that works below. But first, let's do the magic of turning these rules into validation.

First, we use a function from Vuelidate called useVuelidate to merge the rules with our object to validate:

import { useVuelidate } from "@vuelidate/core";

We just call it with the rules and the object:

const model = useVuelidate(rules, payment);

We can then return this model object to the Vue:

return {
      model
    };

Instead of binding directly to the underlying object, bind to the validation model instead:

<div class="form-group">
  <label>Credit Card:</label>
  <input class="form-control" 
          v-model="model.creditCard.$model" />
</div>

Note the (model at the end of the property is important to be the data we're actually two-way binding to. I expect that Vuelidate might remove the ")" from these names as we aren't in the Options API any longer so the magic names aren't required.

The model object itself contains top-level error information so you can do things like disable buttons until valid:

<button
  class="btn btn-success"
  :disabled="model.$invalid"
  @click="processPayment()"
>

For each property, it contains $invalid and an error collection. For me I wrapped this in a little component to show the errors:

<error-span :property="model.creditCard" />

I just pass in the property to show the potential validation errors for:

<span v-if="property.$invalid" class="text-danger">
  <slot>
    <ul class="list-unstyled">
      <li v-for="e in property.$errors" :key="e.$message">
        {{ e.$message }}
      </li>
    </ul>
  </slot>
</span>

By default, the errors aren't validated immediately. You can touch the model manually, but I decided to validate on the submission of the form:

async function processPayment(): Promise<void> {
  if (await model.value.$validate()) {
    // ...
  }
}

If this fails, the validation errors will be populated. I love this. One more thing, custom validations. I mentioned earlier that I wrote a couple of validations that were custom to what I am doing. For an exmaple, let's look at a US Phone number validation I wrote using a regular expression:

export const phone = {
  $validator: value => {
    if (typeof value === 'undefined' || value === null || value === '') {
      return true
    }
    return /^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$/.test(value)
  },
  $message: "Must be a valid phone number."
};

Note it is just an object that has the validator and the message to be shown. The validator is just an arrow function that takes the value and tests it against a regular expression. You can also have validators that take parameter:

export const length = (length) => ({
  $validator: value => {
    if (typeof value === 'undefined' || value === null || value === '') {
      return true
    }
    return value?.length === length;
  }, 
  $message: `Must be exactly ${length}characters long`
});

Notice how the parameter is passed into the object so it can be used in both the $validator and the $message.

It is sure to break as they move to release, but I think it's worth looking at their new approach as it matures. What do you think?