Skip to content

Commit

Permalink
fixes #358 - support "validateBeforeSubmit" with async validators
Browse files Browse the repository at this point in the history
* added `validateAsync` (default: false) form option
* added `isAsync` parameter to FormGenerator.validate()
* FormGenerator.validate() and AbstractField.validate() return Promise when `validateAsync` or `isAsync` is true
* renamed `click` handler to `onClick` in FieldSubmit
* added `onValidationError(model, schema, errors)` to FieldSubmit schema to handle validation errors
* added async validator support for FieldSupport
* changed `each` to `forEach` in various places, as "each" is an alias to "forEach" and "forEach" looks more explicit
* removed call to Vue.util.hyphenate as this is no longer supported by Vue, replaced with equivalent `String.replace` expression
* updated fieldSubmit.spec to add "valid form" and "invalid form" tests, valid forms will always call `onSubmit`, invalid forms will not call `onSubmit` (when validateBeforeSubmit = true)
* various code clean up
  • Loading branch information
zoul0813 committed Dec 15, 2017
1 parent e678a72 commit 5a26ef1
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 95 deletions.
77 changes: 48 additions & 29 deletions src/fields/abstractField.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { get as objGet, each, isFunction, isString, isArray, debounce } from "lodash";
import { get as objGet, forEach, isFunction, isString, isArray, debounce } from "lodash";
import validators from "../utils/validators";
import { slugifyFormID } from "../utils/schema";

Expand Down Expand Up @@ -59,53 +59,72 @@ export default {

methods: {
validate(calledParent) {
// console.log('abstractField', 'validate', calledParent);
this.clearValidationErrors();
let validateAsync = objGet(this.formOptions, "validateAsync", false);

let results = [];

if (this.schema.validator && this.schema.readonly !== true && this.disabled !== true) {
let validators = [];
if (!isArray(this.schema.validator)) {
validators.push(convertValidator(this.schema.validator).bind(this));
} else {
each(this.schema.validator, (validator) => {
forEach(this.schema.validator, (validator) => {
validators.push(convertValidator(validator).bind(this));
});
}

each(validators, (validator) => {
let addErrors = err => {
if (isArray(err))
Array.prototype.push.apply(this.errors, err);
else if (isString(err))
this.errors.push(err);
};

let res = validator(this.value, this.schema, this.model);
if (res && isFunction(res.then)) {
// It is a Promise, async validator
res.then(err => {
if (err) {
addErrors(err);
forEach(validators, (validator) => {
if(validateAsync) {
results.push(validator(this.value, this.schema, this.model));
} else {
let result = validator(this.value, this.schema, this.model);
if(result && isFunction(result.then)) {
result.then((err) => {
if(err) {
this.errors = this.errors.concat(err);
}
let isValid = this.errors.length == 0;
this.$emit("validated", isValid, this.errors, this);
}
});
} else {
if (res)
addErrors(res);
});
} else if(result) {
results = results.concat(result);
}
}
});

}

if (isFunction(this.schema.onValidated)) {
this.schema.onValidated.call(this, this.model, this.errors, this.schema);
}
let handleErrors = (errors) => {
// console.log('abstractField', 'all', errors);
let fieldErrors = [];
forEach(errors, (err) => {
// console.log('abstractField', 'err', err);
if(isArray(err) && err.length > 0) {
fieldErrors = fieldErrors.concat(err);
} else if(isString(err)) {
fieldErrors.push(err);
}
});
// console.log('abstractField', 'fieldErrors', 'final', fieldErrors);
if (isFunction(this.schema.onValidated)) {
this.schema.onValidated.call(this, this.model, fieldErrors, this.schema);
}

let isValid = this.errors.length == 0;
if (!calledParent)
this.$emit("validated", isValid, this.errors, this);
let isValid = fieldErrors.length == 0;
if (!calledParent) {
this.$emit("validated", isValid, fieldErrors, this);
}
this.errors = fieldErrors;
// console.log('abstractField', 'this.errors', this.errors);
return fieldErrors;
};

if(!validateAsync) {
return handleErrors(results);
}

return this.errors;
return Promise.all(results).then(handleErrors);
},

debouncedValidate() {
Expand Down
30 changes: 19 additions & 11 deletions src/fields/core/fieldSubmit.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
<template lang="pug">
input(:id="getFieldID(schema)", type="submit", :value="schema.buttonText", @click="click", :name="schema.inputName", :disabled="disabled", :class="schema.fieldClasses")
input(:id="getFieldID(schema)", type="submit", :value="schema.buttonText", @click="onClick", :name="schema.inputName", :disabled="disabled", :class="schema.fieldClasses")
</template>

<script>
import abstractField from "../abstractField";
import { isFunction } from "lodash";
import { isFunction, isEmpty } from "lodash";
export default {
mixins: [ abstractField ],
methods: {
click() {
if (this.schema.validateBeforeSubmit === true)
{
if (!this.$parent.validate()) {
// There are validation errors. Stop the submit
return;
onClick($event) {
if (this.schema.validateBeforeSubmit === true) {
let errors = this.$parent.validate();
let handleErrors = (errors) => {
if(!isEmpty(errors) && isFunction(this.schema.onValidationError)) {
this.schema.onValidationError(this.model, this.schema, errors);
} else if (isFunction(this.schema.onSubmit)) {
this.schema.onSubmit(this.model, this.schema, $event);
}
};
if(errors && isFunction(errors.then)) {
errors.then(handleErrors);
} else {
handleErrors(errors);
}
} else if (isFunction(this.schema.onSubmit)) {
this.schema.onSubmit(this.model, this.schema, $event);
}
if (isFunction(this.schema.onSubmit))
this.schema.onSubmit(this.model, this.schema);
}
}
};
Expand Down
74 changes: 48 additions & 26 deletions src/formGenerator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,23 @@ div.vue-form-generator(v-if='schema != null')

<script>
// import Vue from "vue";
import {each, isFunction, isNil, isArray, isString} from "lodash";
import { get as objGet, forEach, isFunction, isNil, isArray, isString } from "lodash";
import { slugifyFormID } from "./utils/schema";
// Load all fields from '../fields' folder
let fieldComponents = {};
let coreFields = require.context("./fields/core", false, /^\.\/field([\w-_]+)\.vue$/);
each(coreFields.keys(), (key) => {
forEach(coreFields.keys(), (key) => {
let compName = key.replace(/^\.\//, "").replace(/\.vue/, "");
fieldComponents[compName] = coreFields(key);
});
if (process.env.FULL_BUNDLE) { // eslint-disable-line
let Fields = require.context("./fields/optional", false, /^\.\/field([\w-_]+)\.vue$/);
each(Fields.keys(), (key) => {
forEach(Fields.keys(), (key) => {
let compName = key.replace(/^\.\//, "").replace(/\.vue/, "");
fieldComponents[compName] = Fields(key);
});
Expand All @@ -74,9 +74,10 @@ div.vue-form-generator(v-if='schema != null')
default() {
return {
validateAfterLoad: false,
validateAsync: false,
validateAfterChanged: false,
validationErrorClass: "error",
validationSuccessClass: "",
validationSuccessClass: ""
};
}
},
Expand Down Expand Up @@ -110,7 +111,7 @@ div.vue-form-generator(v-if='schema != null')
fields() {
let res = [];
if (this.schema && this.schema.fields) {
each(this.schema.fields, (field) => {
forEach(this.schema.fields, (field) => {
if (!this.multiple || field.multi === true)
res.push(field);
});
Expand All @@ -121,7 +122,7 @@ div.vue-form-generator(v-if='schema != null')
groups() {
let res = [];
if (this.schema && this.schema.groups) {
each(this.schema.groups, (group) => {
forEach(this.schema.groups, (group) => {
res.push(group);
});
}
Expand All @@ -139,10 +140,11 @@ div.vue-form-generator(v-if='schema != null')
if (newModel != null) {
this.$nextTick(() => {
// Model changed!
if (this.options.validateAfterLoad === true && this.isNewModel !== true)
if (this.options.validateAfterLoad === true && this.isNewModel !== true) {
this.validate();
else
} else {
this.clearValidationErrors();
}
});
}
}
Expand All @@ -152,7 +154,7 @@ div.vue-form-generator(v-if='schema != null')
this.$nextTick(() => {
if (this.model) {
// First load, running validation if neccessary
if (this.options.validateAfterLoad === true && this.isNewModel !== true){
if (this.options.validateAfterLoad === true && this.isNewModel !== true) {
this.validate();
} else {
this.clearValidationErrors();
Expand Down Expand Up @@ -185,7 +187,7 @@ div.vue-form-generator(v-if='schema != null')
}
if (isArray(field.styleClasses)) {
each(field.styleClasses, (c) => baseClasses[c] = true);
forEach(field.styleClasses, (c) => baseClasses[c] = true);
}
else if (isString(field.styleClasses)) {
baseClasses[field.styleClasses] = true;
Expand Down Expand Up @@ -298,7 +300,7 @@ div.vue-form-generator(v-if='schema != null')
if (!res && errors && errors.length > 0) {
// Add errors with this field
errors.forEach((err) => {
forEach(errors, (err) => {
this.errors.push({
field: field.schema,
error: err
Expand All @@ -311,32 +313,52 @@ div.vue-form-generator(v-if='schema != null')
},
// Validating the model properties
validate() {
validate(isAsync = null) {
if(isAsync === null) {
isAsync = objGet(this.options, "validateAsync", false);
}
this.clearValidationErrors();
this.$children.forEach((child) => {
if (isFunction(child.validate))
{
let errors = child.validate(true);
errors.forEach((err) => {
this.errors.push({
field: child.schema,
error: err
});
});
let fields = [];
let results = [];
forEach(this.$children, (child) => {
if (isFunction(child.validate)) {
fields.push(child); // keep track of validated children
results.push(child.validate(true));
}
});
let isValid = this.errors.length == 0;
this.$emit("validated", isValid, this.errors);
return isValid;
let handleErrors = (errors) => {
let formErrors = [];
forEach(errors, (err, i) => {
if(isArray(err) && err.length > 0) {
forEach(err, (error) => {
formErrors.push({
field: fields[i].schema,
error: error,
});
});
}
});
this.errors = formErrors;
let isValid = formErrors.length == 0;
this.$emit("validated", isValid, formErrors);
return isAsync ? formErrors : isValid;
};
if(!isAsync) {
return handleErrors(results);
}
return Promise.all(results).then(handleErrors);
},
// Clear validation errors
clearValidationErrors() {
this.errors.splice(0);
each(this.$children, (child) => {
forEach(this.$children, (child) => {
child.clearValidationErrors();
});
},
Expand Down
3 changes: 2 additions & 1 deletion src/utils/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,9 @@ const validators = {
if (!isNil(field.max) && value.length > field.max) {
err.push(msg(messages.textTooBig, value.length, field.max));
}
} else
} else {
err.push(msg(messages.thisNotText));
}

return err;
},
Expand Down
15 changes: 6 additions & 9 deletions test/unit/specs/VueFormGenerator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -878,8 +878,6 @@ describe("VueFormGenerator.vue", () => {

expect(form.errors).to.be.length(1);
expect(onValidated.callCount).to.be.equal(1);
// console.log(onValidated.getCall(0).args[1][0].field);
// console.log(schema.fields[0]);
expect(onValidated.calledWith(false, [{ field: schema.fields[0], error: "The length of text is too small! Current: 1, Minimum: 3"}] )).to.be.true;
});

Expand Down Expand Up @@ -992,8 +990,6 @@ describe("VueFormGenerator.vue", () => {

expect(form.errors).to.be.length(1);
expect(onValidated.callCount).to.be.equal(1);
// console.log(onValidated.getCall(0).args[1][0].field);
// console.log(schema.fields[0]);
expect(onValidated.calledWith(false, [{ field: schema.fields[0], error: "The length of text is too small! Current: 1, Minimum: 3"}] )).to.be.true;
});

Expand Down Expand Up @@ -1025,12 +1021,13 @@ describe("VueFormGenerator.vue", () => {
label: "Name",
model: "name",
validator(value) {
return new Promise(resolve => {
return new Promise( (resolve) => {
setTimeout(() => {
if (value.length >= 3)
if (value.length >= 3) {
resolve();
else
} else {
resolve([ "Invalid name" ]);
}
}, 50);
});
}
Expand Down Expand Up @@ -1073,7 +1070,7 @@ describe("VueFormGenerator.vue", () => {
});
});

it("should be validation error if model value is not valid", cb => {
it("should be validation error if model value is not valid", (done) => {
onValidated.reset();
vm.model.name = "A";
field.validate();
Expand All @@ -1082,7 +1079,7 @@ describe("VueFormGenerator.vue", () => {
expect(form.errors).to.be.length(1);
expect(onValidated.calledWith(false, [{ field: schema.fields[0], error: "Invalid name"}] )).to.be.true;

cb();
done();
}, 100);
});
});
Expand Down
Loading

0 comments on commit 5a26ef1

Please sign in to comment.