VeeValidate 3.0
This is a draft implementation for version 3.0, which is mostly breaking in many ways but we will try to make it easier to migrate. The changes mostly affect directive usage and will have minimal effects on validation providers.
This document details all the changes done in v3
.
You can follow the implementation PR here #2153
You can test the v3
release with the vee-validate-edge
npm package.
Goals
The goals of this release are as following:
- Rewrite in TypeScript.
v-validate
directive deprecation.
- Overhaul the custom-rules/extension API.
- Overhaul the localization API.
TypeScript
VeeValidate has been TypeScript-friendly for a very long time, but it had its shortcomings. Since it was maintained by contributors - a big shout out to you all - it often wasn't in sync with the actual code. Also, some typings were confusing because they were intended to be internal.
A TypeScript codebase will not only give us more confidence, but it will also communicate the intent of this library APIs clearly to TypeScript/JavaScript users.
Some aspects of vee-validate are impossible to Typecheck properly like the injected errors
and fields
. This is touched upon later.
Directive Deprecation
This is the biggest breaking change and will certainly have some backlash against it, the directive, after all, has been the primary way to use vee-validate since x.0
releases. Even vee-validate
name is a pun for the v-validate
directive.
But to list the directive problems:
- Requires global installation, tightly coupled to the rest of the library.
- Directives do not have props, so to pass stuff around we use
data-vv-*
attributes which aren't reactive, are repetitive and will add clutter your template.
- Forces injected state
errors
and fields
to be present.
v-if
and v-for
caveats which aren't apparent to the developer at first glance.
- Validation Scope is limited within a single component context, it doesn't carry the scope to nested and internal components.
- Complicated grouping with the
Scopes
API which is often confused with Validation Scope
, also adds clutter to your template.
- Probably the only library that advertises the use of the
Provider/Inject
API which is not recommended for public use.
- Templates can get out of hand and noisy really fast and that makes it harder to debug/maintain.
Now a little bit of history, why was the directive the default design choice for this library?
When vee-validate first got released for Vue 1.x the directives was more powerful back then. They had their own props, and they had their own state. Which means the directive was more like a pseudo-component, it was a great choice for this library's API because it did the job perfectly.
When Vue 2.0 got released, it removed props and this
from directives, I believe they were too powerful and a distinction had to be made between them and components, while directives offered a lower level access to the DOM it meant that they had to do less, yet I was stubborn and the API was left as is. I needed to implement external state management and resort to data-vv-*
attributes. It wasn't very clean but it did the job again so the API remained the same, and because we had no better way of doing template based validation, it was fine.
Then scoped-slots
came out, and it took a while for me to properly understand how they can be used to run validation, remember what I said about directives? well, components while "high-level", they offer "low-level" access to Vue's virtual DOM. Which is what I needed exactly and what was missing for the directive.
Now I'm going to talk about the biggest problems with the directive.
Injected State
The need for the injected state is caused by directives being stateless, as they don't have this
and no longer can store stuff on their own. This change was introduced in Vue 2.0 release which was the biggest limitation introduced to directives.
The primary way to use v-validate
directive is to get your errors off an injected global state called errors
, this causes a problem for maintenance sake as it is not immediately clear where did errors
come from, which is the same downsides for using global mixins. Your teammates simply had to know that the project is using vee-validate
.
Being a globally present state, it has poor performance as it changes frequently depending on how many fields are reading/writing to that state. This can be observed if you have 100+ fields on the same page, which while is very rare it still meant that by default vee-validate wasn't performant.
they are impossible to type check properly without breaking somebody's code out there.
That leaves us with one option: Use mapState
like a helper to explicitly inject state, very much like vuex
. I originally intended for the directive to be in v3
and introduced a full-rewrite that implements that. But the API was unwieldy in my opinion and it didn't solve the performance issue, in fact, it made it worse.
v-if
and v-for
caveats
The directive life-cycle methods work most of the time, but when Vue decides to re-use a field to save up rendering time, The directive doesn't get notified in any way. Meaning if the directive was to pick up changes in the template it had to re-check the field it is being attached to every single time before validation, which degrades performance even further as observed in the test implementation I talked about earlier.
Directives weren't meant to be dependent on the identity of the element they are being attached to, they should be dependent solely the expression and the directive args/modifiers configured for it. Which is what was missing for my initial understanding of directives.
Sharing state between components
Using inject
API for sharing state between a parent component and a child component being validated wasn't very intuitive, also the confirmed
rule and any cross-field rule would never work across components as they rely on the refs
present in the context of the v-validate
usage.
This problem comes up weekly, and while we could argue the docs can do a better job to describe this issue, I think by trying to address many caveats it means that the API isn't that good in the first place.
Scope API
the data-vv-scope
API was just ugly, confusing and was redundant most of the time. I personally never used it, and my team at Baianat rarely did and it rarely comes up in issues, which is an indication that it is underused, that means developers did not have multiple forms in the same component that often.
Aside from looks, it also had a hand in performance degradation, because while maybe.field
can be a name for a field called literally "maybe.field" or it could be a field scoped within a "maybe" scope name. That means that every time errors.first
and errors.has
are used, a lot of computation has to be done to determine which case is it. Another smell for bad API.
And since directives do not have access to the rendering phase, it meant that they had no way to prepare some stuff beforehand. For example, to access field flags you need to always check for the field existence first:
fields.name && fields.name.valid
Which is annoying in large forms but there is no way around that, I tried playing around with Proxies, but they cannot be poly-filled and messed up big time with Vue.js dev-tools.
The validator API was a mess
There were actually two validator classes present, one injected in each component and the other would be the one you normally import from vee-validate
and the two had identical public API but it was a mess. for example, validate
and validateAll
did the exact same thing. validateScopes
was just an added confusion, another smell for a bad API.
The alternative
One of the goals of this release is to promote better practices, that meant less magic. So the directive had to go since it inherently promoted a few practices that are considered evil. You have been probably using vee-validate for your production site and it certainly did the job well, but did it feel elegant? There are two ways to make a simple
API, it can be simple but crude or it can be simple as in elegant. I strive for the latter and I'm sure we all do.
For the last few months, I have been advertising the use of ValidationProvider
and ValidationObserver
components recently whenever anyone faces one of the above problems. They will be the primary way to use v-validate
as of the time of this writing. The other way that can be introduced in the future if Vue.js adds new APIs to be built upon like the function API
if it made sense.
At first, glance, using the ValidationProvider is more verbose, but when refactored properly they are much more productive and flexible than the directive. Also, the component API is very powerful and can allow some "magic" to happen. For example, the ValidationObserver
can pick up ValidationProvider
instances no matter how deeply nested they are and even if they are being used internally by other components. That means the Observer is able to correctly represent the state of forms.
Since Observers can be nested, that solved the problem of scoping, as you can group fields together by observers and it is much more cleaner and clearer in the template than the old data-vv-scope
API.
They are superior in every way to the directive and having two APIs means the documentation would be splintered and one would have focused over the other, so the components API survived for being the better one.
VeeValidate Global API
VeeValidate has been re-written to expose a function-based API, this isn't a coincidence with the recent events in Vue.js community, it had been planned for a while.
In short, The Validator
, ErrorBag
classes have been deprecated as they no longer were needed in favor of a new stateless API. So v3
exposes the following functions:
extend
localize
validate
configure
setInteractionMode
ValidationProvider
ValidationObserver
withValidation
All of which are tree-shakable and more friendly to your bundle size.
validate
API
Used to be the verify
API, It is stateless, unlike the old Validator
class, with this function you can run arbitrary validation for values using the vee-validate rule system and asynchronous validation pipeline with an i18n support without having to integrate components or anything in your project. Heck, you could even use it server-side, or if you are feeling a little rebellious you can use it inside other validation libraries like vuelidate.
Common Uses for this are:
- Vuex: as you might want to validate values in actions before committing them to the state.
- Validating values rather than fields, which will come in handy if a certain Vue RFC is implemented ๐.
extend
API
Used to be the Validator.extend
method.
There are a few problems with the current rule system, especially for Date/Cross-field rules:
- Date Validation is clunky and is dependent on
date-fns
implementation which lacks proper timezone support.
- Fields can only target one field at a time which makes it impossible to cross-field-check multiple fields.
- Value transformation/normalization before validation is usually needed, but is redundant if implemented as part of the rule.
- Unnecessary increase to the footprint of vee-validate as date rules are rarely used, compared to
required
.
- Impossible to pass objects reliably as rule parameters instead of an array.
To fix this, date rules were deprecated, it is up to you to implement those rules and the new API will make it much easier. You can also use any JS date library you want, date-fns
, moment
, dayjs
to name a few options.
The value transformation/casting API allows you to prepare the value/params before validating. This plays nicely with cross-field validation, which will only transform a field vid
to its value, this will be one of the few transformations available out of the box and will not be configurable.
This is also handy when you want your field to emit a value of certain structure but want the custom validator to validate a certain aspect of that structure, for example you want to pass { value: 'xyz', id: 5 }
to your model but you only want to validate the value
prop.
The proposed API looks like this:
Validator.extend('rule', {
validate (value, params) {
// validate method, required if its a new rule, optional if already exists.
},
message (fieldName) {
// message, optional.
},
params: [
{ name: 'paramName', isTarget: true }, // applies a locator transform to get the other field value.
{
name: 'otherParam',
cast (value) {
return new Date(value); // cast the param value before using it.
}
},
// this param uses both transforms to locate the other field, then transforms its value.
{
name: 'complex',
isTarget: true,
cast (value) {
return 'whatever'; // Cast the other field value
}
},
immediate: false, // this rule should not trigger when the field is initially validated.
computesRequired: false, // this rule can change the required state of this field.
]
})
Note that the isTarget
prop on the rule itself has been deprecated.
This is rather complex but it will be only used in complex rules. So it won't be used often. Also, it solves the object
vs string
param formatting, since in the string case it would be necessary to know the order of the params, but in objects, it is necessary to know the names of the params. when you provide a params
array, it will always be passed as an object to your rule.
It isn't required tho, you can still ignore the params array entirely and the params will be passed as-is to your rule, if you provide an object it will be passed as an object, same for the string syntax.
Here is a snippet on how you would implement the after
rule:
import { extend } from 'vee-validate';
import { parse, format, isValid, isAfter } from 'date-fns';
extend('after', {
validate (value, { other }) {
const parsed = parse(value, 'some-date-format');
if (!isValid(parsed)) return false;
return isAfter(value, other);
},
// parse the field value.
castValue: value => parse(targetValue, 'some-date-format', new Date(),
params: [
{
name: 'other',
isTarget: true,
cast (targetValue) {
// parse the target field value.
return parse(targetValue, 'some-date-format', new Date());
}
}
]
});
Now its always guaranteed that other
will be a date value and since you are parsing the value yourself in the validate function. It is very straightforward to implement complex rules like these.
The extend
function options are entirely optional. You can progressively extend and add more options or modify them in your rules during the life-cycle of your application.
import { extend } from 'vee-validate';
// A simpler shorthand for passing params.
extend('ruleFn', {
params: ['first', 'second'],
// ... other stuff
});
// New rule without options and passing a validate function directly.
extend('ruleFn', () => {
return false;
});
// Add/Modify configuration for the rule.
extend('ruleFn', {
// options..
});
A11y
VeeValidate offered basic accessibility features in v2
and in v3
it will go a step further.
ariaInput
The ValidationProvider
slot props expose an ariaInput
object which you can bind to your inputs:
<template>
<ValidationProvider rules="required" v-slot="{ aria }">
<input type="text" v-model="value" v-bind="aria" />
<pre>{{ aria }}</pre>
</ValidationProvider>
</template>
<style>
input.invalid {
border: solid 1px red;
}
input.valid {
border: solid 1px green;
}
</style>
ariaMsg
ariaMsg
is another set of aria attributes, but you bind it to your error display element. A full example would look like this:
<ValidationProvider rules="required" v-slot="{ errors, ariaInput, ariaMsg }">
<div>
<input type="text" v-model="values.classes" v-bind="ariaInput">
<pre>{{ ariaInput }}</pre>
<span v-bind="ariaMsg">{{ errors[0] }}</span>
</div>
</ValidationProvider>
localize
API and i18n
The dictionary-based implementation for VeeValidate has been confusing to some, and in the Vue ecosystem, it was not widely used directly. For example, people opted often to use VueI18n
since maintaining two validation APIs was problematic, it meant vee-validate had to provide its own adapter for VueI18n
which is unnecessary kilobytes in the wire for those who use non-Node.js solutions for their SSR like PHP's Laravel.
The new API has to be generic, it means the old dictionary had to be hidden away and used behind the scenes. It also means users should be able to have a way to use any i18n
library they like.
The localize
function is an abstraction for the included simple dictionary, and its signature is identical to the Validator.localize
method of the old API.
import { localize } from 'vee-validate';
localize('ar', {
messages: {
required: 'ูุฐุง ุงูุญูู ู
ุทููุจ'
}
});
The full dictionary looks like this:
localize({
en: {
fields: {
// custom messages for each field.
},
messages: {
// messages for each rule.
},
names: {
// custom field names.
}
}
});
Note that by default vee-validate
does not install any locale or rules, you have to import what you need or opt-in to use the full build which has messages and rules loaded beforehand. but with extra footprint cost.
The localize
method is only useful if you plan to use the internal i18n
dictionary used by vee-validate. But if you want to run your own solution like VueI18n
you could do so while installing rules:
import { extend } from 'vee-validate';
import VueI18n from 'vue-i18n';
import { required } from 'vee-validate/dist/rules';
const i18n = new VueI18n({
locale: 'en',
messages: {
en: {
validation: {
required: 'This field is required'
}
}
}
});
extend('required', {
...required, // rule definition
message (field, values) {
return i8n.t('validation.required');
}
});
Not importing the localize
function will drop it from the bundle because its tree-shakable, and will reduce your bundle size down further.
For convenience, vee-validate messages format was updated to be VueI18n
compliant, for example:
{
max: `The {_field_} may not be greater than {length} characters.`,
max_value: `The {_field_} must be {max} or less.`,
mimes: `The {_field_} must have a valid file type.`,
min: `The {_field_} must be at least {length} characters.`,
min_value: `The {_field_} must be {min} or more.`
}
With that being said, the message generator signature was changed to look like this:
interface ValidationMessageGenerator {
(field: string, values: { [k: string]: any }): string;
}
Which means params
and data
are now merged into the same object. A message now can be either a templated message like shown above or a function that returns a string. That means you can use any i18n
implementation out there.
Bundle-size
Few rules were removed due to their size and they are very easy to implement:
- url
- ip
- ip_or_fqn
- credit_card
- date_format
- after
- before
- date_between
- decimal (often confused with integer and digits)
This means vee-validate no longer depends on validator.js
and will allow you to use whatever version of validator.js
to add the rules you need without conflicts from vee-validate internal copy of it.
Some rules behavior has been changed:
The email
rule implementation was changed to a simpler regex pattern check, so it might be less accurate than v2
releases.
The length
rule will only check if the given string/iterable length matches a specific length that is provided, it will no longer allow max
param to be passed.
The rules represent about 4kb on their own, they will be excluded by default and you will need to import the rules your app will be using. There is a full bundle with all the rules pre-configured. This allows us to add more rules in the future without worrying about the bundle size as it has been the case with 2.x releases.
The bundle size of vee-validate v3 has dropped significantly:
| Default Bundle | 2.x size | 3.0-alpha size |
|--------------------------|----------|----------------|
| Disk | 136kb | 79kb |
| Minified | 58.7kb | 21kb |
| Minified + Gzipped | 16kb | 13kb |
| Full Bundle | 2.x size | 3.0-alpha size |
|--------------------|----------|----------------|
| Disk | 350kb | 84kb |
| Minified | 124kb | 32kb |
| Minified + Gzipped | 31kb | 17kb |
This means vee-validate is no longer costly and still offers many of the features that made it popular.
About v2
VeeValidate 2.x will still be maintained and checked for bugs, all critical issues will be fixed in a timely manner, but will receive slower updates for newer stuff.
Migration
There will be a migration guide detailing the changes and how to replace each feature with its new implementation.
You can follow the progress here: #2153
๐ feature ๐ฏ discussion ๐ฃ breaking