A retake on Vue.js form validation
This article marks the release of vee-validate 2.1.0, a template-based validation framework for Vue.js which mainly uses directives.
Directives in Vue.js offer somewhat low-level access that allows us to manipulate the element bound to the directive.
While very handy to use, directives have limits because they don’t have an instance, or rather a state. Back in Vue 1.x they had state and allowed much more to be done. This was one of the reasons that influenced vee-validate design to be template-based.
Recently I introduced the Verify API which validates values without declaring a field. You can even use the verify method on the server-side. 🤨
import express from 'express';
import bodyParser from 'body-parser';
import { Validator } from 'vee-validate';
const app = express();
app.use(bodyParser.json());
const v = new Validator();
// validate the subscribe request.
function validateReq (req) {
return v.verify(req.body.email, 'required|email');
}
app.post('/subscribe', async (_req_, _res_) => {
const { errors, valid } = await validateReq(req);
if (!valid) {
return res.status(422).json({
errors
});
}
// ...
});
app.listen(3000);
This new API has sparked a new idea for me a few weeks ago:
Can I use components to validate? 🤔
Vue has a component API called scoped slots which allows a component to pass data to its slot in an isolated manner. I used that feature for various purposes in our clients’ works.
VueApollouses them as data providers, which in some cases reduces the JavaScript considerably. Scoped slots offer the ability to create behaviors encapsulated in components and a way to communicate the results.
I began experimenting with render functions, diving into the VNode API. I managed to create a reliable alternative to the directive, I would even say, a better approach.
Validation Provider
This is a component that leverages scoped slots to provide validation for your inputs like Vuetify’s VTextField component. It looks like this:
<ValidationProvider rules="required">
<template slot-scope="{ errors }">
<VTextField v-model="value" :error-messages="errors" />
</template>
</ValidationProvider>
Aside from errors, the slot-scope also contains classes, flags, and aria attributes. Opting in for any of those useful properties is better than implicitly injecting them into your components. It also does not force your template to be structured in a certain way. This API is explicit , clean and flexible.
<ValidationProvider rules="required">
<template slot-scope="{ errors }">
<VTextField v-model="foo" :error-messages="errors" />
</template>
</ValidationProvider>
<ValidationProvider rules="required">
<template slot-scope="{ errors }">
<VTextField v-model="bar" :error-messages="errors" />
</template>
</ValidationProvider>
This can grow to become quite verbose, definitely not pretty in a very large form.
A simple refactoring will make it more attractive. Creating another component wrapped by this one is trivial.
<template>
<ValidationProvider _:rules_="rules">
<template _slot-scope_="{ errors }">
<VTextField _v-model_="innerValue" _:error-messages_="errors" />
</template>
</ValidationProvider>
</template>
<script>
import { ValidationProvider } from 'vee-validate';
export default {
name: 'InputWithValidation',
props: {
rules: [_String_],
value: null
},
components: {
_ValidationProvider_
},
data: () => ({
innerValue: null
}),
watch: {
innerValue (_val_) {
_this_.$emit('input', _val_);
}
}
};
</script>
Refactoring the previous example would make it look like this:
<InputWithValidation _v-model_="foo" _rules_="required" />
<InputWithValidation _v-model_="bar" _rules_="required" />
This is a self-validating component but done right. We can also refactor it in another way using higher-order components.
Higher-Order Components
A High-order function is a function that takes a function and returns a new function, an enhanced one.
Likewise, a higher-order component takes a component and returns a new enhanced component. In our case, we want to add validation to our components. This is where withValidation comes in.
import { VTextField } from 'vuetify/lib';
import { withValidation } from 'vee-validate';
const VInput = withValidation(VTextField, ({ _errors_ }) => ({
'error-messages': errors
}));
You can use the new component in place of your input components. The withValidation function uses the ValidationProvider to “enhance” your component.
The second argument transforms the slot-scope data into props which are passed to the original component.
But, there are some cons to using HoC which I will not touch on in this article. Using either approach is fine.
Using components for validation introduces new problems. Like tracking the current validation state without injections/shared state 🤔.
Here is a concrete example:
disable a button as long as inputs are invalid.
We want to be able to observe our inputs and have something present their state to us. At this point, I thought why not double down on the scoped-slots components thing and add another one_ _🤪.
Validation Observer
This component presents the current state of child providers and their HoC variants.
It looks like this:
<ValidationObserver>
<template _slot-scope_="{ valid }">
<InputWithValidation _v-model_="foo" _rules_="required" />
<InputWithValidation _v-model_="bar" _rules_="required" />
<VBtn _:disabled_="!valid" _@click_="submit">Submit</VBtn>
</template>
</ValidationObserver>
You can also control them by validating them on demand or resetting them using the public methods on the observer instance.
Here is everything in action 🎉
https://medium.com/media/545facb2525dd99dcc8dd888c2121e72/href
You can find more detailed description over in the docs which cover scopes, manual validation, and form resets_._
Perf Matters
By taking advantage of Vue’s virtual DOM and render functions vee-validate is now able to work with ridiculously large forms. larger than before for sure.
A long-standing issue with 800 radio input in the same page would have poor performance, Another that was using 120+ text inputs would cause memory leaks in Edge. Using validation providers had a negligible effect on performance in both cases.
Strictly using these components would eliminate the small overhead of having the global mixin that injects state for the directive.
The Future of VeeValidate
We have experimented with those components in Baianat and so far it is easier to use than the directive, less confusing and everyone seems to love it.
For me, this API lights a bright future for vee-validate. It’s API is smaller than the directive, more reliable, SSR friendly, and efficient 💨.
Currently, I’m working on an experimental branch v3 which removes the old API which reduces the bundle size considerably.
- Full-bundle went down to 34kb gzipped (32% smaller).
- Minimal bundle went down to 11kb (57% smaller 😲).
More improvements can be squeezed out once vee-validate begins shifting towards a more modular approach, providing only what you need/use in your apps, not everything.
As for 2.x versions, they will have both approaches available and will continue improving upon the existing API. It will have feature parity with the 3.x versions.
I would love 😍 to get feedback from the community. What do these changes mean to you?
Top comments (0)