vfg-next
Version:
A schema-based form generator component for Vue.js 3
239 lines (208 loc) • 6.03 kB
JavaScript
import { get as objGet, forEach, isFunction, isString, isArray, debounce, uniqueId, uniq as arrayUniq } from "lodash";
import validators from "../utils/validators";
import { slugifyFormID } from "../utils/schema";
function convertValidator(validator) {
if (isString(validator)) {
if (validators[validator] != null) return validators[validator];
else {
console.warn(`'${validator}' is not a validator function!`);
return null; // caller need to handle null
}
}
return validator;
}
function attributesDirective(el, binding ) {
let attrs = objGet(binding.instance, "schema.attributes", {});
let container = binding.value || "input";
if (isString(container)) {
attrs = objGet(attrs, container) || attrs;
}
forEach(attrs, (val, key) => {
el.setAttribute(key, val);
});
}
export default {
directives: {
attributes: {
beforeMount: attributesDirective,
updated: attributesDirective
}
},
props: {
"vfg":{type:Object,default:undefined},
"model":{type:Object,default:undefined},
"schema":{type:Object,default:undefined},
"formOptions":{type:Object,default:undefined},
"disabled":{type:Boolean,default:false}
},
emits: ["validated", "model-updated"],
data() {
return {
errors: [],
debouncedValidateFunc: null,
debouncedFormatFunc: null
};
},
computed: {
value: {
cache: false,
get() {
let val;
if (isFunction(objGet(this.schema, "get"))) {
val = this.schema.get(this.model);
} else {
val = objGet(this.model, this.schema.model);
}
return this.formatValueToField(val);
},
set(newValue) {
let oldValue = this.value;
newValue = this.formatValueToModel(newValue);
if (isFunction(newValue)) {
newValue(newValue, oldValue);
} else {
this.updateModelValue(newValue, oldValue);
}
}
}
},
methods: {
validate(calledParent) {
this.clearValidationErrors();
let validateAsync = objGet(this.formOptions, "validateAsync", false);
let results = [];
if(!this.schema.validator&&this.schema.required) {
this.schema.validator = validators.required
}
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 {
forEach(this.schema.validator, validator => {
validators.push(convertValidator(validator).bind(this));
});
}
forEach(validators, validator => {
if (validateAsync) {
results.push(validator(this.value, this.schema, this.model, this.formOptions.formData));
} else {
let result = validator(this.value, this.schema, this.model, this.formOptions.formData);
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 (result) {
results = results.concat(result);
}
}
});
}
let handleErrors = (errors) => {
let fieldErrors = [];
forEach(arrayUniq(errors), err => {
if (isArray(err) && err.length > 0) {
fieldErrors = fieldErrors.concat(err);
} else if (isString(err)) {
fieldErrors.push(err);
}
});
if (isFunction(this.schema.onValidated)) {
this.schema.onValidated.call(this, this.model, fieldErrors, this.schema);
}
let isValid = fieldErrors.length === 0;
if (!calledParent) {
this.$emit("validated", isValid, fieldErrors, this);
}
this.errors = fieldErrors;
return fieldErrors;
};
if (!validateAsync) {
return handleErrors(results);
}
return Promise.all(results).then(handleErrors);
},
debouncedValidate() {
if (!isFunction(this.debouncedValidateFunc)) {
this.debouncedValidateFunc = debounce(
this.validate.bind(this),
objGet(this.schema, "validateDebounceTime", objGet(this.formOptions, "validateDebounceTime", 500))
);
}
this.debouncedValidateFunc();
},
updateModelValue(newValue, oldValue) {
let changed = false;
if (isFunction(this.schema.set)) {
this.schema.set(this.model, newValue);
changed = true;
} else if (this.schema.model) {
this.setModelValueByPath(this.schema.model, newValue);
changed = true;
}
if (changed) {
this.$emit("model-updated", newValue, this.schema.model);
if (isFunction(this.schema.onChanged)) {
this.schema.onChanged.call(this, this.model, newValue, oldValue, this.schema);
}
if (objGet(this.formOptions, "validateAfterChanged", false) === true) {
if (objGet(this.schema, "validateDebounceTime", objGet(this.formOptions, "validateDebounceTime", 0)) > 0) {
this.debouncedValidate();
} else {
this.validate();
}
}
}
},
clearValidationErrors() {
this.errors.splice(0);
},
setModelValueByPath(path, value) {
// convert array indexes to properties
let s = path.replace(/\[(\w+)\]/g, ".$1");
// strip a leading dot
s = s.replace(/^\./, "");
let o = this.model;
const a = s.split(".");
let i = 0;
const n = a.length;
while (i < n) {
let k = a[i];
if (i < n - 1)
if (o[k] !== undefined) {
// Found parent property. Step in
o = o[k];
} else {
// Create missing property (new level)
//this.$root.$set(o, k, {});
o = o[k];
}
else {
// Set final property value
//this.$root.$set(o, k, value);
o[k] = value;
return;
}
++i;
}
},
getFieldID(schema, unique = false) {
const idPrefix = objGet(this.formOptions, "fieldIdPrefix", "");
return slugifyFormID(schema, idPrefix) + (unique ? "-" + uniqueId() : "");
},
getFieldClasses() {
return objGet(this.schema, "fieldClasses", []);
},
formatValueToField(value) {
return value;
},
formatValueToModel(value) {
return value;
}
}
};