UNPKG

@appscode/ui-builder

Version:
543 lines (505 loc) 16.6 kB
import { deepClone, compare } from "fast-json-patch"; import { resolve, reactiveSet, getValue } from "@/plugins/json-ref-resolver.js"; import { mapGetters } from "vuex"; export default { components: { UbFormElement: () => import("@/components/form-elements/FormElement.vue").then( (module) => module.default ), UbLabelElement: () => import("@/components/form-elements/LabelElement.vue").then( (module) => module.default ), UbToggleElement: () => import("@/components/form-elements/ToggleElement.vue").then( (module) => module.default ), }, props: { contextObject: { type: Object, default: () => ({}), }, parentDiscriminator: { type: Object, default: () => ({}), }, label: { type: Object, default: () => ({}), }, showLabel: { type: Boolean, default: false, }, ui: { type: Object, default: () => ({}), }, schema: { type: Object, default: () => ({}), }, wholeSchema: { type: Object, default: () => ({}), }, wholeModel: { type: null, default: "", }, value: { type: null, default: "", }, disabled: { type: Boolean, default: false, }, reusableElementCtx: { type: Object, default: () => ({}), }, chartUrl: { type: String, default: "", }, hideForm: { type: Boolean, default: false, }, toggleOption: { type: Object, default: () => ({}), }, }, data() { return { discriminator: {}, modelValue: {}, emitValue: {}, storedValue: {}, isFormHidden: false, // to unwatch the attached watchers at the end unWatchers: {}, }; }, mounted() { this.isFormHidden = this.hideForm; if (this.showToggleOption && this.toggleOption.setInitialStatusFalse) { this.isFormHidden = this.toggleOption.setInitialStatusFalse; } }, destroyed() { // execute the un-watchers Object.values(this.unWatchers).forEach((uw) => { uw(); }); }, computed: { ...mapGetters({ initialModel: "wizard/initialModel", modelJson: "wizard/model", keepEmptyModel: "wizard/keepEmptyModel", }), elements() { return this.ui.elements || []; }, labelText() { return this.$ubt(this.label.text || ""); }, showToggleOption() { return !!this.toggleOption.show; }, }, watch: { "ui.discriminator": { immediate: true, deep: true, handler(n, o) { const diff = compare(deepClone(n || {}), deepClone(o || {})); if (diff.length > 0) { this.initializeDiscriminators(n || {}); } }, }, parentDiscriminator: { immediate: true, deep: true, handler(n) { Object.keys(n).forEach((key) => { this.$set(this.discriminator, key, n[key]); }); }, }, initialModel: { immediate: true, deep: true, handler() { this.initializeModelValueProperties(); }, }, emitValue: { deep: true, handler(n) { // to avoid multiple emit for same object if (JSON.stringify(this.storedValue) !== JSON.stringify(n)) { this.storedValue = deepClone(n); this.$emit("input", deepClone(n)); } }, }, elements: { deep: true, handler() { this.initializeModelValueProperties(); }, }, }, methods: { isEmptyValue(value) { if (value && typeof value === "object") { if (Array.isArray(value)) { // this is array if (value.length) { return value.reduce((s, c) => s && this.isEmptyValue(c), true); } else return true; } else { // this is object const keys = Object.keys(value); if (keys.length) { return keys.reduce( (s, c) => s && this.isEmptyValue(value[c]), true ); } else return true; } } else { // any primitive value if (value !== undefined && value !== null && value !== "") return false; else return true; } }, trimValue(value) { if (value && typeof value === "object") { if (Array.isArray(value)) { // this is array if (value.length) { const trimmedArr = value.map((v) => this.trimValue(v)); const filteredArr = trimmedArr.filter((v) => v); return filteredArr.length ? filteredArr : undefined; } else return undefined; } else { // this is object const keys = Object.keys(value); if (keys.length) { const trimmedOb = {}; keys.forEach((k) => { trimmedOb[k] = this.trimValue(value[k]); }); const filteredOb = {}; keys.forEach((k) => { if (trimmedOb[k]) { filteredOb[k] = trimmedOb[k]; } }); return Object.keys(filteredOb).length ? filteredOb : undefined; } else return undefined; } } else { // any primitive value if (value !== undefined && value !== null && value !== "") return value; else return undefined; } }, updateModelValueEvent(obj) { this.$emit("update-model-value", obj); }, initializeDiscriminators(discriminator) { if (discriminator) { Object.keys(discriminator).forEach((key) => { this.$set(this.discriminator, key, discriminator[key].default); }); } }, initializeModelValueProperties() { const elements = this.elements; const updateModelValue = (keyPath) => { // console.log(" updating for: ", keyPath, deepClone(this.modelValue)); Object.keys(this.modelValue).forEach((key) => { if (key.startsWith(`model#${keyPath}`)) { // get existing value from modeljson const newValue = getValue( deepClone(this.modelJson), key.replace("model#", "") ); const currentValue = deepClone(this.modelValue)[key]; const trimmedNewValue = this.trimValue(deepClone(newValue)); const trimmedCurrentValue = this.trimValue(deepClone(currentValue)); if ( JSON.stringify(trimmedNewValue) !== JSON.stringify(trimmedCurrentValue) ) { this.$set(this.modelValue, key, deepClone(trimmedNewValue)); } } }); }; if (elements) { elements.forEach((element) => { const schema = element.schema; if (schema) { const modelPath = this.getModelPath(schema); const [src, path] = modelPath.split("#"); // no need to assign initial model, because on Cancel button click option page will not be visible // this.$set( // this.modelValue, // modelPath, // this.getValue(modelPath, deepClone(this.initialModel)) // ); this.$set( this.modelValue, modelPath, this.getValue(modelPath, deepClone(this.modelJson)) ); this.attatchWatcherToModelValueKey( modelPath, element.type || "single-step-form" ); // attach watchers to individual properties of modelValue if (src === "model") { // if source is model object, then record the update function to the modelPathWatchers // this will ensure to execute the update function for all the subpaths when any parent path is deleted or updated this.$store.commit("wizard/modelPathWatchers$add", { path, func: updateModelValue, }); } this.attatchWatcherToPath(modelPath); } else if (element.type === "single-step-form") { // for nested single step form this.$set(this.modelValue, JSON.stringify(element.elements), null); } // no need to do anything for buttons }); } }, attatchWatcherToPath(modelPath) { const [src, path] = modelPath.split("#"); let srcOb = ""; if (src === "model") { srcOb += "modelJson"; } else if (src === "discriminator") { srcOb += "discriminator"; } const dottedPath = path.replace(/\//g, "."); const fullPath = srcOb + dottedPath; // attach watcher const unwatch = this.$watch( fullPath, (n) => { const existingValue = this.modelValue[modelPath]; if (JSON.stringify(existingValue) !== JSON.stringify(n)) { this.$set(this.modelValue, modelPath, n); } }, { deep: true } ); // save the un-watcher this.$set(this.unWatchers, modelPath, unwatch); }, attatchWatcherToModelValueKey(key, type) { const unwatch = this.$watch( () => this.modelValue[key], (n) => { const modelPath = key; const [src, path] = modelPath.split("#"); const keepEmpty = getValue(this.keepEmptyModel, path); const value = keepEmpty ? n : this.trimValue(n); if (src === "model") { if (!this.discriminator.$isParentSingleStepFormArray) { // update discriminator or model value only when parent is not single step form // new value differs from existing value, so update the existing value if (type !== "single-step-form") { // if current path is allowed to be empty if (keepEmpty) { const curValue = this.getValue( modelPath, deepClone(this.modelJson) ); // keeping the value as it is. No trimming is performed. if (JSON.stringify(curValue) !== JSON.stringify(value)) { this.$store.commit("wizard/model$update", { path: path, value: value, force: true, }); } } else if (!this.isEmptyValue(value)) { const curValue = this.getValue( modelPath, deepClone(this.modelJson) ); // trimming is required to solve infinite loop but // it allows us to compare current value with value safely const trimmedCurrentValue = this.trimValue(curValue); const trimmedValue = this.trimValue(value); if ( JSON.stringify(trimmedCurrentValue) !== JSON.stringify(trimmedValue) ) { this.$store.commit("wizard/model$update", { path: path, value: trimmedValue, force: true, }); } } else { this.$store.commit("wizard/model$delete", path); } } } } else if (src === "discriminator") { // to get root attribute name const pathPrefix = path && path.split("/")[1]; // get the property name to emit const emitAs = this.ui.discriminator && this.ui.discriminator[pathPrefix] && this.ui.discriminator[pathPrefix].emitAs; if (emitAs) { // add property to emitValue is src is equal to discriminator const prop = path && path.split("/").pop(); // replace the root attribute with emit property const replacedPath = path.replace( `/${pathPrefix}/`, `/${emitAs}/` ); reactiveSet(this.discriminator, replacedPath, value, true); this.$set(this.emitValue, prop, value); } else { // src: https://github.com/flitbit/json-ptr#settarget-pointer-value-force reactiveSet(this.discriminator, path, value, true); } } // add property to emitValue is src is equal to model const prop = path && path.split("/").pop(); if (src === "model") { this.$set(this.emitValue, prop, value); } }, { deep: true, immediate: true, } ); // save the un-watcher this.$set(this.unWatchers, `modelValue.${key}`, unwatch); }, resolveSchema(schema) { if (schema) { const { $ref } = schema; if ($ref) { // reference given const [src, path] = $ref.split("#"); if (src) { if (src === "schema") return resolve(this.wholeSchema, `#${path}`); else if (src === "discriminator") return resolve(this.ui.discriminator, `#${path}`); } return resolve(this.wholeSchema, `#${path}`); } //else if (type === "object") { // // object type schema // return schema.properties; // } else if (type === "array") { // // array type schema // } else return schema; } return undefined; }, resolveSchemaToVmodel(element) { const { schema, elements } = element; if (schema) { return this.getModelPath(schema); } else return JSON.stringify(elements); }, getModelPath(schema) { if (schema) { let modelReference = ""; const { $ref, $model } = schema; if ($model) { // if $model exists, use this // it exists for array elements (index) modelReference = $model; } else if ($ref) { // otherwise there should be $ref object modelReference = $ref; } if (modelReference) { const [src, path] = modelReference.split("#"); const replacedPath = path.replace(/\/properties/g, ""); if (src === "schema") return `model#${replacedPath}`; else if (src === "discriminator") return `discriminator#${replacedPath}`; else if (src === "data") { return `data#${replacedPath}`; } else return replacedPath; } } return undefined; }, getValue(modelPath, modelJson) { if (modelPath) { const [src, path] = modelPath.split("#"); // to resolve modelPath to property name const propertyName = modelPath.split("/").pop(); // for prefilling the new element field if ( this.value[propertyName] !== undefined && this.value[propertyName] !== null ) { return this.value[propertyName]; } else if (src === "model") { // get value from cloned modelJson // clone is necessary otherwise any change in the input will cause this value to change, thus will fail in the handleInputChange function check return getValue(deepClone(modelJson), path); } else if (src === "discriminator") { // src: https://github.com/flitbit/json-ptr#settarget-pointer-value-force return getValue(this.discriminator, path); } } return this.value; }, isRequired(schema) { if (schema) { const { $ref } = schema; if ($ref) { const splitRef = $ref.split("/"); // pop the property name const propName = splitRef.pop(); // discard /properties splitRef.pop(); // get source and path for required array const [src, path] = `${splitRef.join("/")}/required`.split("#"); let requiredArray = []; if (src) { if (src === "schema") requiredArray = resolve(this.wholeSchema, `#${path}`) || []; else if (src === "discriminator") requiredArray = resolve(this.ui.discriminator, `#${path}`) || []; } else { requiredArray = resolve(this.wholeSchema, `#${path}`) || []; } return requiredArray.includes(propName); } else { return false; } } else { return false; } }, }, };