Vest v2.0.0 Release Notes

Release Date: 2020-06-20 // almost 4 years ago
  • 👌 Support stateful validations

    ✅ Vest 2 introduces a big architectural change with the addition of internal state. A new api vest.create('suite_name', testsCallback) initializes a new suite and returns a validate function.

    const validate = vest.create('suite\_name', () =\> {test('fieldName', 'message', () =\> {/\* ... \*/});})
    

    🔀 This internal state is very handy and reduces consumer side boilerplate code, because it merges previously-run partial validation results, meaning that if you use the vest.only() or vest.skip() hooks to only run certain fields, vest will merge your previous validation results with the fields skipped in the current run.

    🔀 In previous versions, you had to do that yourself and merge it in your app's state.

    ✅ This state feature also allows auto-cancellation of outdated async callbacks - if you run your async validation again before a previous run did not finish, vest will internally check which async call did not finish yet - and only call the callbacks from the last run. If those async validations belong to a field that's skipped in the most recent validation, then the test won't be ignored (because it has no newer callback).

    ✅ The stateful validate function now takes params and passes it to your validation tests:

    const validate = vest.create('suite\_name', (data) =\> {test('fieldName', 'message', () =\> {/\* ... \*/});});validate({ username: 'lexapem' })
    

    🆕 New vest hook: vest.get()

    Since vest is now stateful, it means that it can allow you to access your state validation out of context - from a different component, from example - without having to store it in a higher up state.

    // file\_1.jsimport vest from 'vest';const validate = vest.create('car\_purchase', callback);// file\_2.jsimport vest from 'vest';const result = vest.get('car\_purchase');result.hasErrors()
    

    ➕ Added support for nesting tests within a group

    👍 Vest now supports adding another level of nesting to your validations with groups. They allow you to skip/only full blocks from your validation based on your logic - such as current tab, or some userland condition.

    🔀 The benefit of group (over general if/else) is that when skipped - they will also be merged from previous state.

    import vest, { test, group, enforce } from 'vest';vest.create('authentication\_form', data =\> {vest.skip(data.userExists ? 'signUp' : 'signIn');test('userName', "Can't be empty", () =\> {enforce(data.username).isNotEmpty();});test('password', "Can't be empty", () =\> {enforce(data.password).isNotEmpty();});group('signIn', () =\> {test('userName','User not found. Please check if you typed it correctly.',findUserName(data.username));});group('signUp', () =\> {test('email', 'Email already registered', isEmailRegistered(data.email));test('age', 'You must be at least 18 years old to join', () =\> {enforce(data.age).largerThanOrEquals(18);});});});
    

    🔄 Changes related to the introduction of groups

    • 🆕 New result object methods:
    • ⚠ getErrorsByGroup, getWarningsByGroup, hasErrorsByGroup, hasWarningsByGroup

      res.hasErrorsByGroup('groupName') // checks the group in generalres.hasErrorsByGroup('groupName', 'fieldName') // check a given field in the group

    🆕 New Vest utilities

    classNames

    After validating user input, you usually need to also indicate the validation result on the page - most of the times by adding a class to your input element. One of the difficulties you are likely to face is that the logic for setting the class is not always the negation of hasErrors.

    const addIsValidClass = !res.hasErrors('fieldName'); // this does not ALWAYS mean 'valid'
    

    What about when the field is skipped or not validated yet? It does not have errors, so res.hasErrors('fieldName') will return false, and by that logic, you might mistakenly add a is-valid class to your element.

    ✅ In this case you will also need to check if the test actually ran - so:

    const addIsValidClass = res.tests[fieldName] && !res.hasErrors('fieldName');
    

    ✅ But this can get pretty cumbersome when added to multiple fields with different criteria (untested, invalid, hasWarning...).

    This is what vest/classNames is for. It is a tiny utility function, that allows you to specify classnames to be added for each criteria.

    ✅ The way it works is simple. You call classNames with your result object, and the list of classes you want to be added for whenever the field is tested, untested, has warning or is invalid. It then returns a function that when called with a field name, returns a space delimited string of classes. If more than one class applies (both tested and invalid, for example) they will both be added to the string.

    import classNames from 'vest/classNames';import validate from './validation';const res = validate(data);const cn = classNames(res, {untested: 'is-untested', // will only be applied if the provided field did not run yettested: 'some-tested-class', // will only be applied if the provided field did runinvalid: 'my\_invalid\_class', // will only be applied if the provided field ran at least once and has an errrorwarning: 'my\_warning\_class', // will only be applied if the provided field ran at least once and has a warning});const fieldOneClasses = cn('field\_1'); // "is-untested"const fieldTwoClasses = cn('field\_2'); // "some-tested-class my\_invalid\_class"const fieldThreeClasses = cn('field\_3'); // "some-tested-class my\_warning\_class"
    

    enforceExtended

    Along with the existing rules, you might need different business related rules, for email, phone number, credit card validations, and more.

    Business related validations provided with enforceExtended

    • isAlphanumeric
    • isCreditCard
    • isCurrency
    • isEmail
    • isIP
    • isIdentityCard
    • isJSON
    • isLocale
    • isMimeType
    • isMobilePhone
    • isPassportNumber
    • isPostalCode
    • isURL

    Usage:

    import enforce from 'vest/enforceExtended';enforce('[email protected]').isEmail();