formvuelate
Version:
Schema Form Generator
563 lines (468 loc) • 16.2 kB
JavaScript
/**
* formvuelate v3.3.1
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vue')) :
typeof define === 'function' && define.amd ? define(['exports', 'vue'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.Formvuelate = {}, global.Vue));
}(this, (function (exports, vue) { 'use strict';
function useUniqueID () {
let UUID = Math.floor(Math.random() * 1000000000);
const UUIDBindings = new Map();
function getID (model) {
if (UUIDBindings.has(model)) {
return UUIDBindings.get(model)
} else {
UUID++;
UUIDBindings.set(model, UUID);
return UUID
}
}
return {
UUID,
UUIDBindings,
getID
}
}
/**
* Find the elements in the row that have a schema property
* @param {Array} row
* @returns
*/
const findSchemaElementsInRow = (row) => {
return row.filter(el => el.schema)
};
/**
* Find the elements in the top level row of a schema
* that are considered "schema" elements, aka. they have a schema prop
* @param {Array} normalizedSchema
* @returns
*/
const findSchemaElements = (normalizedSchema) => {
for (const row of normalizedSchema) {
const elements = findSchemaElementsInRow(row);
if (elements.length) return elements
}
return []
};
/**
* Traverse a schema recursively and find the schema element
* that matches the given model
* @param {String} model
* @param {Array} normalizedSchema
* @returns
*/
const findElementInSchema = (model, normalizedSchema) => {
const schemaElements = findSchemaElements(normalizedSchema);
const isCorrectElement = el => el?.model === model;
if (!schemaElements.length) {
return null
}
for (const el of schemaElements) {
if (isCorrectElement(el)) return el
// Check the subschemas recursively
const subElement = findElementInSchema(model, normalizeSchema(el.schema));
if (isCorrectElement(subElement)) return subElement
}
return null
};
/**
* Parse a user given schema into FVL internal format
* @param {Array|Object} schema
* @returns
*/
const normalizeSchema = (schema) => {
const arraySchema = Array.isArray(schema)
? schema
: Object.keys(schema).map(model => ({
...schema[model],
model
}));
return arraySchema.map(
field => Array.isArray(field) ? field : [field]
)
};
function useParsedSchema (refSchema, model) {
const { getID } = useUniqueID();
const parsedSchema = vue.computed(() => {
const schema = vue.unref(refSchema);
let normalizedSchema = normalizeSchema(schema);
if (model) {
/**
* If the model is provided, it means a SchemaForm is trying to find
* a subschema in the main schema that corresponds to its "model" in the
* use provided schema. We dig into the sub schemas to find it and normalize it
* before setting it as the returned parsed schema
*/
const element = findElementInSchema(model, normalizedSchema);
if (element) {
normalizedSchema = normalizeSchema(element.schema);
}
}
return normalizedSchema.map(fieldGroup => {
return fieldGroup.map(field => ({
...field,
uuid: getID(field.model)
}))
})
});
return {
parsedSchema
}
}
const KEY = 'fvl_';
const IS_SCHEMA_WIZARD = `${KEY}isSchemaWizard`;
const PARENT_SCHEMA_EXISTS = `${KEY}parentSchemaExists`;
const INJECTED_SCHEMA = `${KEY}injectedSchema`;
const SCHEMA_MODEL_PATH = `${KEY}schemaModelPath`;
const FORM_MODEL = `${KEY}formModel`;
const FIND_NESTED_FORM_MODEL_PROP = `${KEY}findNestedFormModelProp`;
const UPDATE_FORM_MODEL = `${KEY}updateFormModel`;
const DELETE_FORM_MODEL_PROP = `${KEY}deleteFormModelProp`;
var constants = /*#__PURE__*/Object.freeze({
__proto__: null,
IS_SCHEMA_WIZARD: IS_SCHEMA_WIZARD,
PARENT_SCHEMA_EXISTS: PARENT_SCHEMA_EXISTS,
INJECTED_SCHEMA: INJECTED_SCHEMA,
SCHEMA_MODEL_PATH: SCHEMA_MODEL_PATH,
FORM_MODEL: FORM_MODEL,
FIND_NESTED_FORM_MODEL_PROP: FIND_NESTED_FORM_MODEL_PROP,
UPDATE_FORM_MODEL: UPDATE_FORM_MODEL,
DELETE_FORM_MODEL_PROP: DELETE_FORM_MODEL_PROP
});
var script$2 = {
name: 'SchemaField',
props: {
field: {
type: Object,
required: true
},
sharedConfig: {
type: Object,
default: () => ({})
},
preventModelCleanupOnSchemaChange: {
type: Boolean,
default: false
}
},
setup (props) {
const binds = vue.computed(() => {
return props.field.schema
? {
// For sub SchemaForm elements
...props.field,
nestedSchemaModel: props.field.model
}
: { ...props.sharedConfig, ...props.field }
});
const formModel = vue.inject(FORM_MODEL, {});
const path = vue.inject(SCHEMA_MODEL_PATH, null);
const findNestedFormModelProp = vue.inject(FIND_NESTED_FORM_MODEL_PROP);
const fieldValue = vue.computed(() => {
if (path) {
return findNestedFormModelProp(path)[props.field.model]
}
return formModel.value[props.field.model]
});
const updateFormModel = vue.inject(UPDATE_FORM_MODEL);
const deleteFormModelProperty = vue.inject(DELETE_FORM_MODEL_PROP);
const update = (value) => {
updateFormModel(props.field.model, value, path);
};
const schemaCondition = vue.computed(() => {
const condition = props.field.condition;
if (!condition || typeof condition !== 'function') return true
return condition(formModel.value)
});
vue.watch(schemaCondition, shouldDisplay => {
if (shouldDisplay) return
if (props.preventModelCleanupOnSchemaChange) return
deleteFormModelProperty(props.field.model, path);
});
return {
binds,
fieldValue,
update,
schemaCondition
}
}
};
function render$2(_ctx, _cache, $props, $setup, $data, $options) {
return ($setup.schemaCondition)
? (vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent($props.field.component), vue.mergeProps({ key: 0 }, $setup.binds, {
modelValue: $setup.fieldValue,
"onUpdate:modelValue": $setup.update,
class: "schema-col"
}), null, 16 /* FULL_PROPS */, ["modelValue", "onUpdate:modelValue"]))
: vue.createCommentVNode("v-if", true)
}
script$2.render = render$2;
script$2.__file = "packages/formvuelate/src/SchemaField.vue";
function useParentSchema () {
const isChildOfWizard = vue.inject(IS_SCHEMA_WIZARD, false);
const hasParentSchema = vue.inject(PARENT_SCHEMA_EXISTS, false);
if (!hasParentSchema) {
vue.provide(PARENT_SCHEMA_EXISTS, true);
}
const behaveLikeParentSchema = vue.computed(() => (!isChildOfWizard && !hasParentSchema));
return {
behaveLikeParentSchema,
hasParentSchema,
isChildOfWizard
}
}
function useInjectedSchema (props, behaveLikeParentSchema) {
const { schema } = vue.toRefs(props);
let injectedSchema = vue.inject(INJECTED_SCHEMA, false);
if (behaveLikeParentSchema) {
// Only the top level schema form should inject the schema
// That way we dont have to worry about injecting the prop down into
// sub schemas
vue.provide(INJECTED_SCHEMA, schema);
injectedSchema = schema;
}
if (props.nestedSchemaModel) {
// If the nestedSchemaModel prop is set it means this
// component is a subschema, and we need to inform descendants
// of the "path" for the model. ex. "info.family.parents"
const path = vue.inject(SCHEMA_MODEL_PATH, '');
const currentPath = path ? `${path}.${props.nestedSchemaModel}` : props.nestedSchemaModel;
vue.provide(SCHEMA_MODEL_PATH, currentPath);
}
return {
schema: injectedSchema
}
}
function useFormModel (props, parsedSchema) {
const formModel = vue.inject(FORM_MODEL, {});
const cleanupModelChanges = (schema, oldSchema) => {
if (props.preventModelCleanupOnSchemaChange) return
const reducer = (acc, val) => {
return acc.concat(val.map(i => i.model))
};
const newKeys = schema.reduce(reducer, []);
const oldKeys = oldSchema.reduce(reducer, []);
const diff = oldKeys.filter(i => !newKeys.includes(i));
if (!diff.length) return
for (const key of diff) {
delete formModel.value[key];
}
};
vue.watch(parsedSchema, cleanupModelChanges);
}
var script$1 = {
name: 'SchemaForm',
components: { SchemaField: script$2 },
props: {
schema: {
type: [Object, Array],
required: true,
validator (schema) {
if (!Array.isArray(schema)) return true
return schema.filter(field => !Array.isArray(field) && (!field.model && !field.schema)).length === 0
}
},
sharedConfig: {
type: Object,
default: () => ({})
},
preventModelCleanupOnSchemaChange: {
type: Boolean,
default: false
},
nestedSchemaModel: {
type: String,
default: ''
},
schemaRowClasses: {
type: [String, Object, Array],
default: null
}
},
emits: ['submit', 'update:modelValue'],
setup (props, { emit, attrs }) {
const { behaveLikeParentSchema, hasParentSchema } = useParentSchema();
const { schema } = useInjectedSchema(props, behaveLikeParentSchema);
const { parsedSchema } = useParsedSchema(schema, attrs.model);
useFormModel(props, parsedSchema);
const formBinds = vue.computed(() => {
if (hasParentSchema) return {}
return {
onSubmit: event => {
event.preventDefault();
emit('submit', event);
}
}
});
const slotBinds = vue.computed(() => {
return {}
});
return {
behaveLikeParentSchema,
parsedSchema,
hasParentSchema,
formBinds,
slotBinds
}
}
};
function render$1(_ctx, _cache, $props, $setup, $data, $options) {
const _component_SchemaField = vue.resolveComponent("SchemaField");
return (vue.openBlock(), vue.createBlock(vue.resolveDynamicComponent($setup.behaveLikeParentSchema ? 'form' : 'div'), $setup.formBinds, {
default: vue.withCtx(() => [
($setup.behaveLikeParentSchema)
? vue.renderSlot(_ctx.$slots, "beforeForm", vue.mergeProps({ key: 0 }, $setup.slotBinds))
: vue.createCommentVNode("v-if", true),
(vue.openBlock(true), vue.createBlock(vue.Fragment, null, vue.renderList($setup.parsedSchema, (fields, index) => {
return (vue.openBlock(), vue.createBlock("div", {
class: ['schema-row', $props.schemaRowClasses],
key: index
}, [
(vue.openBlock(true), vue.createBlock(vue.Fragment, null, vue.renderList(fields, (field) => {
return (vue.openBlock(), vue.createBlock(_component_SchemaField, {
key: field.model,
field: field,
sharedConfig: $props.sharedConfig,
preventModelCleanupOnSchemaChange: $props.preventModelCleanupOnSchemaChange,
class: "schema-col"
}, null, 8 /* PROPS */, ["field", "sharedConfig", "preventModelCleanupOnSchemaChange"]))
}), 128 /* KEYED_FRAGMENT */))
], 2 /* CLASS */))
}), 128 /* KEYED_FRAGMENT */)),
($setup.behaveLikeParentSchema)
? vue.renderSlot(_ctx.$slots, "afterForm", vue.mergeProps({ key: 1 }, $setup.slotBinds))
: vue.createCommentVNode("v-if", true)
]),
_: 3 /* FORWARDED */
}, 16 /* FULL_PROPS */))
}
script$1.render = render$1;
script$1.__file = "packages/formvuelate/src/SchemaForm.vue";
var script = {
name: 'SchemaWizard',
components: { SchemaForm: script$1 },
props: {
schema: {
type: Array,
required: true
},
step: {
type: Number,
required: true,
default: 0
}
},
emits: ['submit'],
setup (props) {
vue.provide(IS_SCHEMA_WIZARD, true);
const currentSchema = vue.computed(() => {
return props.schema[props.step]
});
return { currentSchema }
}
};
function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_SchemaForm = vue.resolveComponent("SchemaForm");
return (vue.openBlock(), vue.createBlock("form", {
onSubmit: _cache[1] || (_cache[1] = vue.withModifiers($event => (_ctx.$emit('submit', $event)), ["prevent"]))
}, [
vue.renderSlot(_ctx.$slots, "beforeForm"),
vue.createVNode(_component_SchemaForm, {
schema: $setup.currentSchema,
preventModelCleanupOnSchemaChange: ""
}, null, 8 /* PROPS */, ["schema"]),
vue.renderSlot(_ctx.$slots, "afterForm")
], 32 /* HYDRATE_EVENTS */))
}
script.render = render;
script.__file = "packages/formvuelate/src/SchemaWizard.vue";
function SchemaFormFactory (plugins = [], components = null) {
// Copy the original SchemaForm setup
const originalSetup = script$1.setup;
function setup (props, context) {
// Call the original setup and preserve its results
const baseSchemaFormReturns = originalSetup(props, context);
if (!plugins.length) return baseSchemaFormReturns
else {
// Apply plugins on the data returned
// by the original Schemaform
return plugins.reduce(
(schemaFormReturns, plugin) => {
return plugin(schemaFormReturns, props, context)
},
baseSchemaFormReturns
)
}
}
const SchemaFieldWithComponents = {
...script$2,
components: {
...components,
...script$2.components
}
};
return {
...script$1,
components: {
...components,
...script$1.components,
SchemaField: SchemaFieldWithComponents
},
// Return a customized setup function with plugins
// as the new SchemaForm setup
setup
}
}
/**
* Find a key inside an object, or create it if it doesn't exist
* @param {Object} model
* @param {String} key
* @returns
*/
const findOrCreateProp = (model, key) => {
if (!model[key]) {
model[key] = {};
}
return model[key]
};
function useSchemaForm (initialFormValue = {}) {
const formModel = vue.isRef(initialFormValue) ? initialFormValue : vue.ref(initialFormValue);
const findNestedFormModelProp = (path) => {
const keys = path.split('.');
let nestedProp = findOrCreateProp(formModel.value, keys[0]);
for (let i = 1; i < keys.length; i++) {
nestedProp = findOrCreateProp(nestedProp, keys[i]);
}
return nestedProp
};
const updateFormModel = (prop, value, path) => {
if (!path) {
formModel.value[prop] = value;
return
}
findNestedFormModelProp(path)[prop] = value;
};
const deleteFormModelProperty = (prop, path) => {
if (!path) {
delete formModel.value[prop];
return
}
delete findNestedFormModelProp(path)[prop];
};
vue.provide(UPDATE_FORM_MODEL, updateFormModel);
vue.provide(DELETE_FORM_MODEL_PROP, deleteFormModelProperty);
vue.provide(FIND_NESTED_FORM_MODEL_PROP, findNestedFormModelProp);
vue.provide(FORM_MODEL, formModel);
return {
formModel
}
}
exports.SchemaForm = script$1;
exports.SchemaFormFactory = SchemaFormFactory;
exports.SchemaWizard = script;
exports.constants = constants;
exports['default'] = script$1;
exports.useSchemaForm = useSchemaForm;
Object.defineProperty(exports, '__esModule', { value: true });
})));