UNPKG

form-generator-vue

Version:

Create beautiful forms using any component library for vue. ## Features * reactive schema based form. * compatible with third party component libraries like vuetify, element etc and custom components. * customizable form layout. ## [Demo](https://divijbha

709 lines (610 loc) 20.3 kB
var props = { props: { value: { type: Object, default: null, required: false }, onSubmit: { type: Function, required: false, default: () => { console.error("submit handler not present"); } }, components: { type: Array, required: false, default: () => [] }, disabled: { type: Boolean, required: false, default: false }, schema: { type: Object, default: () => ({}) }, classes: { type: Object, required: false, default: () => ({}) }, onSubmitFail: { type: Function, required: false, default: () => { console.warn("Form submit fail"); } }, activeValidation: { type: Boolean, required: false, default: false }, activeValidationDelay: { type: Number, required: false, default: 0 }, logs: { type: Boolean, required: false, default: false } } }; let debounce_timeout; const UTILS = { isUndef(val) { return typeof val === "undefined"; }, isObjNotArr(val) { if (!UTILS.isArr(val)) { return UTILS.isObj(val) && !UTILS.isArr(val); } return val.every(v => UTILS.isObj(v) && !UTILS.isArr(v)); }, isObj(val) { if (!UTILS.isArr(val)) { return typeof val === 'object'; } return val.every(v => typeof v === 'object'); }, isArr(val) { return Array.isArray(val); }, isFunc(val) { return typeof val === "function"; }, isBool(val) { return typeof val === "boolean"; }, isStr(val) { return typeof val === 'string'; }, throwError(msg) { throw new Error(msg); }, warn(msg) { console.warn(msg); }, hasProperty(children, parent) { if (!UTILS.isArr(children)) { return children in parent; } return children.every(child => child in parent); }, handleFunc(func, params = undefined) { if (UTILS.isFunc(func)) { return func(params); } }, handleFuncOrBool(val, funcParams = undefined) { let res = Boolean(val); if (UTILS.isFunc(val)) { res = val(funcParams); } return res; }, debounce(func) { return function (time) { return function exeFunction(p) { clearTimeout(debounce_timeout); debounce_timeout = setTimeout(function () { clearTimeout(debounce_timeout); func(p); }, time); }; }; } }; const CLASS = { form: 'fgv-form', header: `fgv-form__header`, body: `fgv-form__body`, footer: `fgv-form__footer`, row: `fgv-form__body__row`, col: `fgv-form__body__row__col` }; const SLOT = { header: 'header', footer: 'footer', beforeComponent: v => `before-${v}`, afterComponent: v => `after-${v}`, beforeRow: 'before-row', afterRow: 'after-row', beforeCol: 'before-col', afterCol: 'after-col' }; const SCHEMA = { fields: 'fields', av: 'activeValidation', avDelay: 'activeValidationDelay', logs: 'logs' }; const VMODEL = { values: 'values', errors: 'errors' }; const FIELD = { av: SCHEMA.av, avDelay: SCHEMA.avDelay, events: 'v-on', component: 'component', hide: 'hide', type: { text: 'text', number: 'number' }, props: { required: 'required', disabled: 'disabled' } }; // var script = { mixins: [props], data() { const INIT = true; let fields = {}; let errors = {}; const addFieldsAndErrors = model => { // on init if v-model has values then validate and apply those values. fields[model] = this.vModelValid(INIT) && VMODEL.values in this.value && this.value[VMODEL.values][model] || ''; errors[model] = this.vModelValid(INIT) && VMODEL.errors in this.value && this.value[VMODEL.errors][model] || ''; }; if (SCHEMA.fields in this.schema && UTILS.isArr(this.schema.fields) && this.schema.fields.length) { for (const schema of this.schema.fields) { if (UTILS.isArr(schema)) { for (const s of schema) { addFieldsAndErrors(s.model); } } else { addFieldsAndErrors(schema.model); } } } return { fields, errors, submit: false }; }, computed: { SLOT: () => SLOT, CLASS: () => CLASS, UTILS: () => UTILS, avGlobal() { // return SCHEMA.av in this.schema // ? this.schema[SCHEMA.av] // : false; return this.activeValidation || false; }, avDelayGlobal() { // const hasAvDelay = SCHEMA.avDelay in this.schema && this.schema[SCHEMA.avDelay] && !isNaN(this.schema[SCHEMA.avDelay]); // return hasAvDelay? this.schema[SCHEMA.avDelay] : false; return this.activeValidationDelay || 0; }, // logs() { // return SCHEMA.logs in this.schema ? this.schema[SCHEMA.logs] : false; // }, fieldsSchema() { return SCHEMA.fields in this.schema && UTILS.isArr(this.schema[SCHEMA.fields]) ? this.schema[SCHEMA.fields] : []; }, fieldsSchemaFlat() { let flatSchema = []; for (const schema of this.fieldsSchema) { if (UTILS.isArr(schema)) { for (const s of schema) { flatSchema.push(s); } } else { flatSchema.push(schema); } } return flatSchema; }, fieldsSchemaMap() { const map = this.fieldsSchemaFlat.map(s => [s.model, s]); return Object.fromEntries(map); }, deValidateField() { return UTILS.debounce(model => { this.validateField(model); }); } }, watch: { disabled: { handler: function (newVal) { newVal && this.removeAllErrors(); } }, value: { handler: function () { if (this.vModelValid()) { for (const model in this.value[VMODEL.values]) { this.fields[model] = this.value[VMODEL.values][model]; this.errors[model] = this.value[VMODEL.errors][model]; } } }, deep: true }, fields: { handler: function () { this.rmUnwantedModels(); this.$emit("input", { values: this.fields, errors: this.errors }); }, deep: true, immediate: true } }, created() { for (const model in this.fields) { const schema = this.findSchema(model); this.$watch(`fields.${model}`, function (newVal, oldVal) { // for number type field. this.typeCoercion(schema); // this.updateHelpers(model, newVal); // to prevent below calls when only type is changed and not value. if (newVal == oldVal && typeof newVal !== typeof oldVal) { return; } // validation --------------------------- this.validate(schema, true); }, { deep: true }); } }, methods: { slotProps(schema) { if (UTILS.isArr()) { return schema.map(({ model }) => model); } return schema.model; }, validate(schema = undefined, watcher = false) { // watcher if (schema && watcher) { const avField = Boolean(schema[FIELD.av]) || this.avGlobal; const avDelayField = schema && schema[FIELD.avDelay] || this.avDelayGlobal; avField && avDelayField ? this.deValidateField(avDelayField)(schema) : this.validateField(schema); return; } // on submit const status = {}; Object.values(this.fieldsSchemaMap).forEach(s => { const err = this.validateField(s); status[s.model] = !err ? true : !this.fieldRequired(s); }); const fail = Object.keys(status).find(k => !status[k]); return [status, fail]; }, showRow(schema) { return this.hasFieldsToRender(schema) || this.showCol(schema); }, hasFieldsToRender(schema) { return UTILS.isArr(schema) && schema.length && schema.some(s => !this.fieldHidden(s)); }, showCol(schema) { return this.componentToRender(schema) && !this.fieldHidden(schema); }, vModelValid(init = false) { const parentValid = this.value && UTILS.isObjNotArr(this.value); const valValid = VMODEL.values in this.value && UTILS.isObjNotArr(this.value[VMODEL.values]); const errValid = VMODEL.errors in this.value && UTILS.isObjNotArr(this.value[VMODEL.errors]); if (init) { return parentValid && valValid; } return parentValid && valValid && errValid; }, resetFormState() { this.submit = false; }, removeAllErrors() { for (const model in this.errors) { this.errors[model] = ""; } }, setError(model, e) { const oldErr = this.errors[model]; if (oldErr === e || UTILS.isObj(e, oldErr) && JSON.stringify(e) === JSON.stringify(oldErr)) { return; } this.errors[model] = e; }, findComponentData(name) { return this.components.find(c => c && c.name === name); }, componentProps(schema) { const componentName = this.componentToRender(schema); const component = this.findComponentData(componentName); const errorPropName = schema && schema.errorProp || component && component.errorProp || 'errorMessages'; return { ...schema.props, [errorPropName]: this.errors[schema.model], ref: schema.model, type: schema.type || FIELD.type.text, disabled: this.fieldDisabled(schema), required: this.fieldRequired(schema) }; }, typeCoercion(schema) { if (!isNaN(this.fields[schema.model])) { return; } // const schema = this.findSchema(model); schema && schema.type === FIELD.type.number && this.fields[schema.model] && (this.fields[schema.model] = Number(this.fields[schema.model])); }, componentEvents(schema) { return FIELD.events in schema && UTILS.isObj(schema[FIELD.events]) ? schema[FIELD.events] : {}; }, componentToRender(schema) { const fieldType = schema.type || FIELD.type.text; if (FIELD.component in schema && schema[FIELD.component] && UTILS.isStr(schema[FIELD.component])) { return schema.component; } const component = this.components.find(({ type }) => type.includes(fieldType)); const componentName = component && component.name; !componentName && console.error(`Component cannot be rendered. Component for type "${fieldType}" is not found in form-components.`); return componentName; }, findSchema(m) { // return this.fieldsSchemaFlat.find(({model}) => m === model); return this.fieldsSchemaMap[m]; }, fieldDisabled(schema) { const DISABLED = true; const hasDisabledProp = schema && schema.props && FIELD.props.disabled in schema.props; const fieldDisabled = hasDisabledProp ? UTILS.handleFuncOrBool(schema.props[FIELD.props.disabled]) : !DISABLED; return this.disabled || fieldDisabled ? DISABLED : !DISABLED; }, fieldRequired(schema) { const REQUIRED = true; // const model = m || s.model; // const schema = s || this.findSchema(model); const hasRequiredProp = schema && schema.props && FIELD.props.required in schema.props; const fieldRequired = hasRequiredProp ? UTILS.handleFuncOrBool(schema.props[FIELD.props.required]) : 'validator' in schema ? REQUIRED : !REQUIRED; // : !this.isHelperComponent(model); return schema && !this.fieldDisabled(schema) && !this.fieldHidden(schema) ? fieldRequired : !REQUIRED; }, rmUnwantedModels() { const uf = Object.keys(this.fields).filter(m => !this.fieldsSchemaFlat.find(({ model }) => m === model)); uf.forEach(model => { delete this.fields[model]; delete this.errors[model]; }); }, fieldHidden(schema) { const HIDDEN = true; const fieldHidden = FIELD.hide in schema ? UTILS.handleFuncOrBool(schema[FIELD.hide]) : !HIDDEN; // !fieldVisible && this.setDefaultFieldValue(schema); return fieldHidden; }, validateField(schema) { const VALID = ''; // const schema = this.findSchema(model); const fieldRequired = this.fieldRequired(schema); const validator = schema && schema.validator; const avField = Boolean(schema[FIELD.av]) || this.avGlobal; const error = this.submit || avField ? UTILS.handleFunc(validator) || VALID : VALID; const valid = !error ? VALID : Boolean(error); !fieldRequired ? !this.submit && this.setError(schema.model, error) : this.setError(schema.model, error); this.logs && console.log({ model: schema.model, value: this.fields[schema.model], type: typeof this.fields[schema.model], valid, required: fieldRequired, error }); return valid; }, async handleSubmit() { this.submit = true; this.rmUnwantedModels(); const [status, fail] = this.validate(); if (this.logs) { console.log("form validations:", status); } if (fail) { this.resetFormState(); await this.onSubmitFail(); return; } await this.onSubmit(); this.resetFormState(); } } }; function normalizeComponent(template, style, script, scopeId, isFunctionalTemplate, moduleIdentifier /* server only */, shadowMode, createInjector, createInjectorSSR, createInjectorShadow) { if (typeof shadowMode !== 'boolean') { createInjectorSSR = createInjector; createInjector = shadowMode; shadowMode = false; } // Vue.extend constructor export interop. const options = typeof script === 'function' ? script.options : script; // render functions if (template && template.render) { options.render = template.render; options.staticRenderFns = template.staticRenderFns; options._compiled = true; // functional template if (isFunctionalTemplate) { options.functional = true; } } // scopedId if (scopeId) { options._scopeId = scopeId; } let hook; if (moduleIdentifier) { // server build hook = function (context) { // 2.3 injection context = context || // cached call (this.$vnode && this.$vnode.ssrContext) || // stateful (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext); // functional // 2.2 with runInNewContext: true if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { context = __VUE_SSR_CONTEXT__; } // inject component styles if (style) { style.call(this, createInjectorSSR(context)); } // register component module identifier for async chunk inference if (context && context._registeredComponents) { context._registeredComponents.add(moduleIdentifier); } }; // used by ssr in case component is cached and beforeCreate // never gets called options._ssrRegister = hook; } else if (style) { hook = shadowMode ? function (context) { style.call(this, createInjectorShadow(context, this.$root.$options.shadowRoot)); } : function (context) { style.call(this, createInjector(context)); }; } if (hook) { if (options.functional) { // register for functional component in vue file const originalRender = options.render; options.render = function renderWithStyleInjection(h, context) { hook.call(context); return originalRender(h, context); }; } else { // inject component registration as beforeCreate hook const existing = options.beforeCreate; options.beforeCreate = existing ? [].concat(existing, hook) : [hook]; } } return script; } /* script */ const __vue_script__ = script; /* template */ var __vue_render__ = function () { var _vm = this; var _h = _vm.$createElement; var _c = _vm._self._c || _h; return _c('form', { class: [_vm.CLASS.form], on: { "submit": function ($event) { $event.preventDefault(); return _vm.handleSubmit($event); } } }, [_c('div', { class: [_vm.CLASS.header] }, [_vm._t(_vm.SLOT.header)], 2), _vm._v(" "), _c('div', { class: [_vm.CLASS.body] }, [_vm._l(_vm.fieldsSchema, function (schema, i) { return [_vm.showRow(schema) ? _vm._t(_vm.SLOT.beforeRow, null, { "model": _vm.slotProps(schema) }) : _vm._e(), _vm._v(" "), _vm.showRow(schema) ? _c('div', { key: i, class: [_vm.CLASS.row, _vm.classes.row] }, [!_vm.UTILS.isArr(schema) ? [_vm.showCol(schema) ? _vm._t(_vm.SLOT.beforeCol, null, { "model": _vm.slotProps(schema) }) : _vm._e(), _vm._v(" "), _vm.showCol(schema) ? _c('div', { key: schema.model, class: [_vm.CLASS.col, schema.model, _vm.classes.col] }, [_vm._t(_vm.SLOT.beforeComponent(schema.model)), _vm._v(" "), _c(_vm.componentToRender(schema), _vm._g(_vm._b({ tag: "component", model: { value: _vm.fields[schema.model], callback: function ($$v) { _vm.$set(_vm.fields, schema.model, $$v); }, expression: "fields[schema.model]" } }, 'component', _vm.componentProps(schema), false), _vm.componentEvents(schema)), [_vm._t(schema.model)], 2), _vm._v(" "), _vm._t(_vm.SLOT.afterComponent(schema.model))], 2) : _vm._e(), _vm._v(" "), _vm.showCol(schema) ? _vm._t(_vm.SLOT.afterCol, null, { "model": _vm.slotProps(schema) }) : _vm._e()] : [_vm._l(schema, function (s) { return [_vm.showCol(s) ? _vm._t(_vm.SLOT.beforeCol, null, { "model": _vm.slotProps(s) }) : _vm._e(), _vm._v(" "), _vm.showCol(s) ? _c('div', { key: s.model, class: [_vm.CLASS.col, s.model, _vm.classes.col] }, [_vm._t(_vm.SLOT.beforeComponent(s.model)), _vm._v(" "), _c(_vm.componentToRender(s), _vm._g(_vm._b({ tag: "component", model: { value: _vm.fields[s.model], callback: function ($$v) { _vm.$set(_vm.fields, s.model, $$v); }, expression: "fields[s.model]" } }, 'component', _vm.componentProps(s), false), _vm.componentEvents(s)), [_vm._t(s.model)], 2), _vm._v(" "), _vm._t(_vm.SLOT.afterComponent(s.model))], 2) : _vm._e(), _vm._v(" "), _vm.showCol(s) ? _vm._t(_vm.SLOT.afterCol, null, { "model": _vm.slotProps(s) }) : _vm._e()]; })]], 2) : _vm._e(), _vm._v(" "), _vm.showRow(schema) ? _vm._t(_vm.SLOT.afterRow, null, { "model": _vm.slotProps(schema) }) : _vm._e()]; })], 2), _vm._v(" "), _c('div', { class: _vm.CLASS.footer }, [_vm._t(_vm.SLOT.footer)], 2)]); }; var __vue_staticRenderFns__ = []; /* style */ const __vue_inject_styles__ = undefined; /* scoped */ const __vue_scope_id__ = undefined; /* module identifier */ const __vue_module_identifier__ = undefined; /* functional template */ const __vue_is_functional_template__ = false; /* style inject */ /* style inject SSR */ /* style inject shadow dom */ const __vue_component__ = /*#__PURE__*/normalizeComponent({ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ }, __vue_inject_styles__, __vue_script__, __vue_scope_id__, __vue_is_functional_template__, __vue_module_identifier__, false, undefined, undefined, undefined); // Import vue component const install = function installFormGeneratorVue(Vue) { if (install.installed) return; install.installed = true; Vue.component('FormGeneratorVue', __vue_component__); }; // Create module definition for Vue.use() // to be registered via Vue.use() as well as Vue.component() __vue_component__.install = install; // Export component by default // also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo'; // export const RollupDemoDirective = component; export default __vue_component__;