Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

VeeValidate v3.0 馃殌 #2191

Closed
logaretm opened this issue Jul 29, 2019 · 25 comments

Comments

@logaretm
Copy link
Owner

@logaretm logaretm commented Jul 29, 2019

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

@logaretm logaretm pinned this issue Jul 29, 2019
@logaretm logaretm changed the title VeeValidate v3.0 VeeValidate v3.0 馃殌 Jul 29, 2019
@fessacchiotto

This comment has been minimized.

Copy link

@fessacchiotto fessacchiotto commented Aug 15, 2019

The scope could be useful. If for example you are using a tabbed panel/form, and every tab is a component, you could detect an error in any component and disable, for example, the whole form submit button. But let's assume the error is in a tab which is hidden, and thus the user sees the disabled form submit button, but doesn't understand in which tab the error may be. Instead, if you assign a scope to all fields of a given tab of the form, you could quickly check in what scope the error is and notify the user by, for example, highlighting in red the tab title. Thank you for your work!

@fessacchiotto

This comment has been minimized.

Copy link

@fessacchiotto fessacchiotto commented Aug 15, 2019

Is there any timeframe for a beta of the version 3 release?

@logaretm

This comment has been minimized.

Copy link
Owner Author

@logaretm logaretm commented Aug 15, 2019

@fessacchiotto You could use multiple observers and have them under one parent observer, which would give you what you need as you could check the whole state of the forms or individual tabs by checking each observer.

I will include this use-case in the docs as I think its fairly common.

You can use the current Provider/Observer components as they didn't change a lot in v3. Probably I will skip a beta version since I'm confident in v3 production readiness.

@kristremblay

This comment has been minimized.

Copy link

@kristremblay kristremblay commented Aug 22, 2019

Is there any timeframe for a beta of the version 3 release?

@logaretm Thank you for the update on #1844, is it reasonable to estimate a release schedule for v3?

@logaretm

This comment has been minimized.

Copy link
Owner Author

@logaretm logaretm commented Aug 23, 2019

vee-validate is out, looking forward to your feedback!

@logaretm logaretm closed this Aug 23, 2019
@kristremblay

This comment has been minimized.

Copy link

@kristremblay kristremblay commented Aug 23, 2019

@logaretm Thanks for the heads up! I will give it a shot Monday and see :)

@Loremaster

This comment has been minimized.

Copy link

@Loremaster Loremaster commented Aug 30, 2019

@logaretm is migration guide to version 3 available anywhere? I couldn't find it.

@bodograumann

This comment has been minimized.

Copy link

@bodograumann bodograumann commented Aug 30, 2019

Have not seen one either, but here is an instructive article: https://www.baianat.com/labs/code/veevalidate-3-0 @Loremaster

@Loremaster

This comment has been minimized.

Copy link

@Loremaster Loremaster commented Aug 30, 2019

@bodograumann thanks for that! I have seen that already and was hoping for more in depth guide :)

@logaretm

This comment has been minimized.

Copy link
Owner Author

@logaretm logaretm commented Aug 30, 2019

@Loremaster

This comment has been minimized.

Copy link

@Loremaster Loremaster commented Aug 30, 2019

@logaretm Thank you! Will be waiting for that.

@Loremaster

This comment has been minimized.

Copy link

@Loremaster Loremaster commented Sep 18, 2019

@logaretm any updates on the migration guide?

@DM2489

This comment has been minimized.

Copy link
Contributor

@DM2489 DM2489 commented Oct 7, 2019

I too am looking for a migration guide. We moved away from the directive a long time ago in favour of validation components, and I'm more interested in adding in a few rules that were removed.

How would the url rule be implemented in 3.0?

@robsonsobral

This comment has been minimized.

Copy link

@robsonsobral robsonsobral commented Oct 21, 2019

@logaretm , please, is there something close to a migration guide? Thank you!

@victor-ponamariov

This comment has been minimized.

Copy link

@victor-ponamariov victor-ponamariov commented Oct 22, 2019

The new version has removed some rules which are easy to implement, however, as I can see from https://github.com/validatorjs/validator.js/blob/master/src/lib/isURL.js here, it's not that easy actually to implement url rule :(

@3zzy

This comment has been minimized.

Copy link

@3zzy 3zzy commented Oct 28, 2019

@logaretm Really appreciate all the effort that went into v3 and look forwarding to using it, however a migration guide would be really helpful.

@RomainMazB

This comment has been minimized.

Copy link

@RomainMazB RomainMazB commented Oct 31, 2019

@victor-ponamariov > my workaround to implement it:

extend('url', {
        validate: (str) => {
            var pattern = new RegExp('^(https?:\\/\\/)?'+ // protocol
                '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // domain name
                '((\\d{1,3}\\.){3}\\d{1,3}))'+ // OR ip (v4) address
                '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // port and path
                '(\\?[;&a-z\\d%_.~+=-]*)?'+ // query string
                '(\\#[-a-z\\d_]*)?$','i'); // fragment locator
            return !!pattern.test(str);
        },
        message: 'This is not a valid URL'
    })

Regex from Zemljoradnik

@Loremaster

This comment has been minimized.

Copy link

@Loremaster Loremaster commented Nov 5, 2019

@logaretm any updates?

@mattsteve

This comment has been minimized.

Copy link

@mattsteve mattsteve commented Nov 10, 2019

While I understand all the reasons behind your design decisions, I will stick with using v2 because it doesn't inject random tags in the DOM that mess with the styling and layout of the form.

@fessacchiotto

This comment has been minimized.

Copy link

@fessacchiotto fessacchiotto commented Nov 10, 2019

@robsonsobral

This comment has been minimized.

Copy link

@robsonsobral robsonsobral commented Nov 10, 2019

@mattsteve, random tags? Don't we have control over them?

@logaretm

This comment has been minimized.

Copy link
Owner Author

@logaretm logaretm commented Nov 10, 2019

@mattsteve It's far from random, they always render a span and you can specify slim property to have it render the contents only.

@mattsteve

This comment has been minimized.

Copy link

@mattsteve mattsteve commented Nov 10, 2019

@logaretm "random" in terms of "not designed for the layout". I was unaware of the slim prop, but at the same time it's not a perfect solution since it only takes 1 child.

@arneysebaert

This comment has been minimized.

Copy link

@arneysebaert arneysebaert commented Nov 13, 2019

Any update on that migration guide? I don't see how we can update our version to v3 without any form of guide...

@logaretm

This comment has been minimized.

Copy link
Owner Author

@logaretm logaretm commented Nov 13, 2019

I'm working on it at the moment as part of the new docs, the changes can be tracked here: https://github.com/logaretm/vee-validate/tree/docs/change-structure

I'm trying to build a more interactive experience in the migration guide, as it is a totally different concept and there is no simple steps to follow. Also, vee-validate surface area is large as it handles localization and arbitrary validation as well, so covering the changes in those areas is tricky.

I'm aiming to release 3.1.0 with the new docs by the end of this weekend (Saturday).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can鈥檛 perform that action at this time.