vue-generators / vue-form-generator

:clipboard: A schema-based form generator component for Vue.js
MIT License
2.99k stars 532 forks source link

Add form wide validation #330

Closed will-fgmnt closed 5 years ago

will-fgmnt commented 6 years ago

Current validation is field level only. It would be great if there was the ability to validate the form as a whole with the error message appear above or below the form (preferably configurable).

My use case is I have a password and passwordConfirm fields which I want to match. Putting a validator on one or both fields won't work as they are dependant on each other.

An alternative approach would be a formOption which runs all field validator whenever any field changes.

zoul0813 commented 6 years ago

Here's a custom validator I wrote for comparing two fields, and a JSFiddle example

https://jsfiddle.net/zoul0813/rukxozdk/ - using _.isEqual for more complex comparisons

https://jsfiddle.net/zoul0813/rukxozdk/4/ - without lodash, doing simple "a" == "b" check

function isEqualTo(value, field, model) {
    if(field.equals == undefined)
        return ['invalid field schema, missing `equals` property'];
    let a = model[field.equals];
    if(value == a) 
        return [];
    return ['strings do not match'];
}

Just add this to your project, then add isEqualTo to the validator array for the second password. Add an equals property to the second password fields schema pointing to the first passwords model.

[{
  type: 'input',
  inputType: 'password',
  label: 'Password',
  model: 'password',
  min: 5,
  max: 10,
  required: true,
  validator: ['string']
},
{
  type: 'input',
  inputType: 'password',
  label: 'Password (Confirm)',
  model: 'password2',
  min: 5,
  max: 10,
  required: true,
  equals: 'password',
  validator: ['string', isEqualTo]
}]
icebob commented 6 years ago

Nice solution

will-fgmnt commented 6 years ago

@zoul0813 Thanks. ~Your solution will work but it's worth mentioning you must add the isEqualTo validator on both fields~ EDIT: Actually, no this won't work because validation is still per-field.

Consider the following:

  1. Type mypass into password field.
  2. Type mypass into password2 field.
  3. Edit password field and change mypass to mypass1.

The password fields no longer match but no error is shown.

I do strongly believe having form level validation would be a good addition. For example say you had a form with several checkboxes where certain combinations of checks are valid while some combinations are not. Giving each field it's own unique validator might get unwieldly quite quickly as opposed to a single form level validator which accepts the model as it's value and returns an error object if invalid (basically what validate.js does).

zoul0813 commented 6 years ago

@will-fgmnt, the validation function is passed the full form model, and the advanced case of multiple checks would require a custom validator rule anyhow. Whenever validation occurs on a field, the validated event is fired on the form.

I’m just started working on a Validate.js wrapper for VFG this morning, and should have something “usable” tomorrow or the next day as my current project requires some advanced validation as well. I’ll keep you posted on my progress.

zoul0813 commented 6 years ago

@icebob, @will-fgmnt here's a rough draft of what I'm working on.

https://jsfiddle.net/zoul0813/z3up3rwo/

Basically just a Proxy wrapper for ValidateJs, it looks at the field's rules property. ValidateJs.equality or any of the other validators works. You can also run multiple ValidateJs validations just by adding them to the "rules" property and calling one of the virtual functions to trigger them all (the rules property is just passed to validate() as the constraint). For simple usage, the function called on ValidateJs creates a "functionName": true constraint (for things like { email: true }).

This is just a rough draft, and I plan to clean this up quite a bit ... I also hope to be able to get "form-wide validation" working so that when a field is changed, all the fields revalidate. Likely going to stick with the "field by field" validation, but plan to figure out how to get the fields to revalidate themselves by listening for a validation event.

// ValidateJs Proxy for vue-form-generator
// Use ValidateJs.equality, ValidateJs.length, ValidateJs.presence, ValidateJs.format, etc for the field `validator`
let ValidateJsObj = {}
window.ValidateJs = new Proxy(ValidateJsObj, {
    get: function get(target, name) {
        return function wrapper(value, field, model) {
            let constraint = {};
            let rules = field['rules'];
            if(rules == undefined) {
                rules = {};
                rules[name] = true;
            }
            constraint[field['model']] = rules;
            let errors = validate(model, constraint);
            if(errors != undefined && errors[field['model']])
                return errors[field['model']]
            return [];
        }
    }
});
zoul0813 commented 5 years ago

Closing this issue

You can validate the entire form before submitting or using the model-updated event (just call vfg.validate() inside of your handler), or write custom validation functions. field-by-field validation is really the only way to achieve validation as "form-level" validation would add a large amount of overhead (re-validating everything each time one item is changed).

Here's my final "ValidateJs" wrapper for VFG as well:

// ValidateJs "Plugin" for VFG
import _ from 'lodash';
import validate from 'validate.js';

validate.Promise = Promise;
let ValidateJsObj = {};
window.ValidateJs = new Proxy(ValidateJsObj, {
    get(target, name) {
        return (value, schema, model) => {
            let constraint = {};
            let rules = {
                [name]: _.get(schema.rules, name, true)
            };
            constraint[schema.model] = rules;
            return validate.async(
                model, 
                constraint, 
                {
                    cleanAttributes: false,
                    format: "flat"
                }
            ).then((errors) => {
                return [];
            }).catch((err) => {
                console.error('ValidateJs:catch', err);
                return err;
            });
        };
    }
});

You can then use this in a VFG schema like this:

{
    "type": "input",
    "inputType": "password",
    "autocomplete": "new-password",
    "label": "Preferred Password",
    "model": "password",
    "hint": "Must be 8+ characters and contain at least one uppercase and one number",
    "min": 8,
    "required": true,
    "rules": {
        "format": {
            "pattern": "^(((?=.*[A-Z])((?=.*[0-9])|(?=.*[!@#\\$%\\^&\\*])))).{8,}",
            "flags": "",
            "message": "must contain at least one uppercase character and one number or special character"
        }
    },
    "validator": ["string", ValidateJs.format]
},
{
    "type": "input",
    "inputType": "password",
    "autocomplete": "new-password",
    "label": "Confirm Password",
    "model": "password_confirm",
    "required": true,
    "rules": {
        "equality": "password"
    },
    "validator": ["required", ValidateJs.equality]
}