@appscode/ui-builder
Version:
## Motivation
543 lines (505 loc) • 16.6 kB
JavaScript
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;
}
},
},
};