Skip to content

Commit 5a26ef1

Browse files
committed
fixes #358 - support "validateBeforeSubmit" with async validators
* 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
1 parent e678a72 commit 5a26ef1

File tree

7 files changed

+159
-95
lines changed

7 files changed

+159
-95
lines changed

src/fields/abstractField.js

+48-29
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { get as objGet, each, isFunction, isString, isArray, debounce } from "lodash";
1+
import { get as objGet, forEach, isFunction, isString, isArray, debounce } from "lodash";
22
import validators from "../utils/validators";
33
import { slugifyFormID } from "../utils/schema";
44

@@ -59,53 +59,72 @@ export default {
5959

6060
methods: {
6161
validate(calledParent) {
62+
// console.log('abstractField', 'validate', calledParent);
6263
this.clearValidationErrors();
64+
let validateAsync = objGet(this.formOptions, "validateAsync", false);
65+
66+
let results = [];
6367

6468
if (this.schema.validator && this.schema.readonly !== true && this.disabled !== true) {
6569
let validators = [];
6670
if (!isArray(this.schema.validator)) {
6771
validators.push(convertValidator(this.schema.validator).bind(this));
6872
} else {
69-
each(this.schema.validator, (validator) => {
73+
forEach(this.schema.validator, (validator) => {
7074
validators.push(convertValidator(validator).bind(this));
7175
});
7276
}
7377

74-
each(validators, (validator) => {
75-
let addErrors = err => {
76-
if (isArray(err))
77-
Array.prototype.push.apply(this.errors, err);
78-
else if (isString(err))
79-
this.errors.push(err);
80-
};
81-
82-
let res = validator(this.value, this.schema, this.model);
83-
if (res && isFunction(res.then)) {
84-
// It is a Promise, async validator
85-
res.then(err => {
86-
if (err) {
87-
addErrors(err);
78+
forEach(validators, (validator) => {
79+
if(validateAsync) {
80+
results.push(validator(this.value, this.schema, this.model));
81+
} else {
82+
let result = validator(this.value, this.schema, this.model);
83+
if(result && isFunction(result.then)) {
84+
result.then((err) => {
85+
if(err) {
86+
this.errors = this.errors.concat(err);
87+
}
8888
let isValid = this.errors.length == 0;
8989
this.$emit("validated", isValid, this.errors, this);
90-
}
91-
});
92-
} else {
93-
if (res)
94-
addErrors(res);
90+
});
91+
} else if(result) {
92+
results = results.concat(result);
93+
}
9594
}
9695
});
97-
9896
}
9997

100-
if (isFunction(this.schema.onValidated)) {
101-
this.schema.onValidated.call(this, this.model, this.errors, this.schema);
102-
}
98+
let handleErrors = (errors) => {
99+
// console.log('abstractField', 'all', errors);
100+
let fieldErrors = [];
101+
forEach(errors, (err) => {
102+
// console.log('abstractField', 'err', err);
103+
if(isArray(err) && err.length > 0) {
104+
fieldErrors = fieldErrors.concat(err);
105+
} else if(isString(err)) {
106+
fieldErrors.push(err);
107+
}
108+
});
109+
// console.log('abstractField', 'fieldErrors', 'final', fieldErrors);
110+
if (isFunction(this.schema.onValidated)) {
111+
this.schema.onValidated.call(this, this.model, fieldErrors, this.schema);
112+
}
103113

104-
let isValid = this.errors.length == 0;
105-
if (!calledParent)
106-
this.$emit("validated", isValid, this.errors, this);
114+
let isValid = fieldErrors.length == 0;
115+
if (!calledParent) {
116+
this.$emit("validated", isValid, fieldErrors, this);
117+
}
118+
this.errors = fieldErrors;
119+
// console.log('abstractField', 'this.errors', this.errors);
120+
return fieldErrors;
121+
};
122+
123+
if(!validateAsync) {
124+
return handleErrors(results);
125+
}
107126

108-
return this.errors;
127+
return Promise.all(results).then(handleErrors);
109128
},
110129

111130
debouncedValidate() {

src/fields/core/fieldSubmit.vue

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
<template lang="pug">
2-
input(:id="getFieldID(schema)", type="submit", :value="schema.buttonText", @click="click", :name="schema.inputName", :disabled="disabled", :class="schema.fieldClasses")
2+
input(:id="getFieldID(schema)", type="submit", :value="schema.buttonText", @click="onClick", :name="schema.inputName", :disabled="disabled", :class="schema.fieldClasses")
33
</template>
44

55
<script>
66
import abstractField from "../abstractField";
7-
import { isFunction } from "lodash";
7+
import { isFunction, isEmpty } from "lodash";
88
99
export default {
1010
mixins: [ abstractField ],
1111
1212
methods: {
13-
click() {
14-
if (this.schema.validateBeforeSubmit === true)
15-
{
16-
if (!this.$parent.validate()) {
17-
// There are validation errors. Stop the submit
18-
return;
13+
onClick($event) {
14+
if (this.schema.validateBeforeSubmit === true) {
15+
let errors = this.$parent.validate();
16+
let handleErrors = (errors) => {
17+
if(!isEmpty(errors) && isFunction(this.schema.onValidationError)) {
18+
this.schema.onValidationError(this.model, this.schema, errors);
19+
} else if (isFunction(this.schema.onSubmit)) {
20+
this.schema.onSubmit(this.model, this.schema, $event);
21+
}
22+
};
23+
24+
if(errors && isFunction(errors.then)) {
25+
errors.then(handleErrors);
26+
} else {
27+
handleErrors(errors);
1928
}
29+
} else if (isFunction(this.schema.onSubmit)) {
30+
this.schema.onSubmit(this.model, this.schema, $event);
2031
}
21-
22-
if (isFunction(this.schema.onSubmit))
23-
this.schema.onSubmit(this.model, this.schema);
2432
}
2533
}
2634
};

src/formGenerator.vue

+48-26
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,23 @@ div.vue-form-generator(v-if='schema != null')
3737

3838
<script>
3939
// import Vue from "vue";
40-
import {each, isFunction, isNil, isArray, isString} from "lodash";
40+
import { get as objGet, forEach, isFunction, isNil, isArray, isString } from "lodash";
4141
import { slugifyFormID } from "./utils/schema";
4242
4343
// Load all fields from '../fields' folder
4444
let fieldComponents = {};
4545
4646
let coreFields = require.context("./fields/core", false, /^\.\/field([\w-_]+)\.vue$/);
4747
48-
each(coreFields.keys(), (key) => {
48+
forEach(coreFields.keys(), (key) => {
4949
let compName = key.replace(/^\.\//, "").replace(/\.vue/, "");
5050
fieldComponents[compName] = coreFields(key);
5151
});
5252
5353
if (process.env.FULL_BUNDLE) { // eslint-disable-line
5454
let Fields = require.context("./fields/optional", false, /^\.\/field([\w-_]+)\.vue$/);
5555
56-
each(Fields.keys(), (key) => {
56+
forEach(Fields.keys(), (key) => {
5757
let compName = key.replace(/^\.\//, "").replace(/\.vue/, "");
5858
fieldComponents[compName] = Fields(key);
5959
});
@@ -74,9 +74,10 @@ div.vue-form-generator(v-if='schema != null')
7474
default() {
7575
return {
7676
validateAfterLoad: false,
77+
validateAsync: false,
7778
validateAfterChanged: false,
7879
validationErrorClass: "error",
79-
validationSuccessClass: "",
80+
validationSuccessClass: ""
8081
};
8182
}
8283
},
@@ -110,7 +111,7 @@ div.vue-form-generator(v-if='schema != null')
110111
fields() {
111112
let res = [];
112113
if (this.schema && this.schema.fields) {
113-
each(this.schema.fields, (field) => {
114+
forEach(this.schema.fields, (field) => {
114115
if (!this.multiple || field.multi === true)
115116
res.push(field);
116117
});
@@ -121,7 +122,7 @@ div.vue-form-generator(v-if='schema != null')
121122
groups() {
122123
let res = [];
123124
if (this.schema && this.schema.groups) {
124-
each(this.schema.groups, (group) => {
125+
forEach(this.schema.groups, (group) => {
125126
res.push(group);
126127
});
127128
}
@@ -139,10 +140,11 @@ div.vue-form-generator(v-if='schema != null')
139140
if (newModel != null) {
140141
this.$nextTick(() => {
141142
// Model changed!
142-
if (this.options.validateAfterLoad === true && this.isNewModel !== true)
143+
if (this.options.validateAfterLoad === true && this.isNewModel !== true) {
143144
this.validate();
144-
else
145+
} else {
145146
this.clearValidationErrors();
147+
}
146148
});
147149
}
148150
}
@@ -152,7 +154,7 @@ div.vue-form-generator(v-if='schema != null')
152154
this.$nextTick(() => {
153155
if (this.model) {
154156
// First load, running validation if neccessary
155-
if (this.options.validateAfterLoad === true && this.isNewModel !== true){
157+
if (this.options.validateAfterLoad === true && this.isNewModel !== true) {
156158
this.validate();
157159
} else {
158160
this.clearValidationErrors();
@@ -185,7 +187,7 @@ div.vue-form-generator(v-if='schema != null')
185187
}
186188
187189
if (isArray(field.styleClasses)) {
188-
each(field.styleClasses, (c) => baseClasses[c] = true);
190+
forEach(field.styleClasses, (c) => baseClasses[c] = true);
189191
}
190192
else if (isString(field.styleClasses)) {
191193
baseClasses[field.styleClasses] = true;
@@ -298,7 +300,7 @@ div.vue-form-generator(v-if='schema != null')
298300
299301
if (!res && errors && errors.length > 0) {
300302
// Add errors with this field
301-
errors.forEach((err) => {
303+
forEach(errors, (err) => {
302304
this.errors.push({
303305
field: field.schema,
304306
error: err
@@ -311,32 +313,52 @@ div.vue-form-generator(v-if='schema != null')
311313
},
312314
313315
// Validating the model properties
314-
validate() {
316+
validate(isAsync = null) {
317+
if(isAsync === null) {
318+
isAsync = objGet(this.options, "validateAsync", false);
319+
}
315320
this.clearValidationErrors();
316321
317-
this.$children.forEach((child) => {
318-
if (isFunction(child.validate))
319-
{
320-
let errors = child.validate(true);
321-
errors.forEach((err) => {
322-
this.errors.push({
323-
field: child.schema,
324-
error: err
325-
});
326-
});
322+
let fields = [];
323+
let results = [];
324+
325+
forEach(this.$children, (child) => {
326+
if (isFunction(child.validate)) {
327+
fields.push(child); // keep track of validated children
328+
results.push(child.validate(true));
327329
}
328330
});
329331
330-
let isValid = this.errors.length == 0;
331-
this.$emit("validated", isValid, this.errors);
332-
return isValid;
332+
let handleErrors = (errors) => {
333+
let formErrors = [];
334+
forEach(errors, (err, i) => {
335+
if(isArray(err) && err.length > 0) {
336+
forEach(err, (error) => {
337+
formErrors.push({
338+
field: fields[i].schema,
339+
error: error,
340+
});
341+
});
342+
}
343+
});
344+
this.errors = formErrors;
345+
let isValid = formErrors.length == 0;
346+
this.$emit("validated", isValid, formErrors);
347+
return isAsync ? formErrors : isValid;
348+
};
349+
350+
if(!isAsync) {
351+
return handleErrors(results);
352+
}
353+
354+
return Promise.all(results).then(handleErrors);
333355
},
334356
335357
// Clear validation errors
336358
clearValidationErrors() {
337359
this.errors.splice(0);
338360
339-
each(this.$children, (child) => {
361+
forEach(this.$children, (child) => {
340362
child.clearValidationErrors();
341363
});
342364
},

src/utils/validators.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,9 @@ const validators = {
117117
if (!isNil(field.max) && value.length > field.max) {
118118
err.push(msg(messages.textTooBig, value.length, field.max));
119119
}
120-
} else
120+
} else {
121121
err.push(msg(messages.thisNotText));
122+
}
122123

123124
return err;
124125
},

test/unit/specs/VueFormGenerator.spec.js

+6-9
Original file line numberDiff line numberDiff line change
@@ -878,8 +878,6 @@ describe("VueFormGenerator.vue", () => {
878878

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

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

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

@@ -1025,12 +1021,13 @@ describe("VueFormGenerator.vue", () => {
10251021
label: "Name",
10261022
model: "name",
10271023
validator(value) {
1028-
return new Promise(resolve => {
1024+
return new Promise( (resolve) => {
10291025
setTimeout(() => {
1030-
if (value.length >= 3)
1026+
if (value.length >= 3) {
10311027
resolve();
1032-
else
1028+
} else {
10331029
resolve([ "Invalid name" ]);
1030+
}
10341031
}, 50);
10351032
});
10361033
}
@@ -1073,7 +1070,7 @@ describe("VueFormGenerator.vue", () => {
10731070
});
10741071
});
10751072

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

1085-
cb();
1082+
done();
10861083
}, 100);
10871084
});
10881085
});

0 commit comments

Comments
 (0)