Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions src/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@
<span id="formeo-logo-wrap"></span>
</header>
<section id="main_content" class="inner">
<form class="build-form clearfix"></form>
<div class="render-form">
<h2>Rendered Form</h2>
</div>
<form class="build-form"></form>
<div class="render-form"></div>
</section>
<div class="container render-btn-wrap" id="editor-action-buttons">
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/lib/js/components/autocomplete.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ export const labelCount = (arr, label) => {
* @return {String} component label
*/
const getComponentLabel = ({ name, id, ...component }) => {
const labelPaths = ['config.label', 'attrs.id', 'meta.id']
const labelPaths = ['config.label', 'config.controlId', 'meta.id', 'attrs.id']
const label = labelPaths.reduce((acc, cur) => {
if (!acc) {
acc = component.get(cur)
return component.get(cur)
}
return acc
}, null)
Expand Down
31 changes: 25 additions & 6 deletions src/lib/js/components/component-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,30 @@ import { uuid, clone, merge } from '../common/utils/index.mjs'
import { get } from '../common/utils/object.mjs'

export default class ComponentData extends Data {
load = (data = Object.create(null)) => {
load = dataArg => {
const data = this.parseformData(dataArg)
this.empty()
if (typeof data === 'string') {
data = JSON.parse(data)
for (const [key, val] of Object.entries(data)) {
this.add(key, val)
}
Object.entries(data).forEach(([key, val]) => this.add(key, val))
return this.data
}

/**
* Retrieves data from the specified path or adds new data if no path is provided.
*
* @param {string} [path] - The path to retrieve data from. If not provided, new data will be added.
* @returns {*} The data retrieved from the specified path or the result of adding new data.
*/
get = path => (path ? get(this.data, path) : this.add())

/**
* Adds a new component with the given id and data.
*
* @param {string} id - The unique identifier for the component. If not provided, a new UUID will be generated.
* @param {Object} [data=Object.create(null)] - The data to initialize the component with.
* @returns {Object} The newly created component.
*/
add = (id, data = Object.create(null)) => {
const elemId = id || uuid()
const component = this.Component({ ...data, id: elemId })
Expand All @@ -29,16 +42,22 @@ export default class ComponentData extends Data {
*/
remove = componentId => {
if (Array.isArray(componentId)) {
componentId.forEach(id => {
for (const id of componentId) {
this.get(id).remove()
})
}
} else {
this.get(componentId).remove()
}

return this.data
}

/**
* Deletes a component from the data object.
*
* @param {string} componentId - The ID of the component to delete.
* @returns {string} The ID of the deleted component.
*/
delete = componentId => {
delete this.data[componentId]
return componentId
Expand Down
32 changes: 25 additions & 7 deletions src/lib/js/components/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import Components from './index.js'
import Data from './data.js'
import animate from '../common/animation.js'
import Controls from './controls/index.js'
import { get } from '../common/utils/object.mjs'
import { get, set } from '../common/utils/object.mjs'
import { toTitleCase } from '../common/utils/string.mjs'

export default class Component extends Data {
Expand Down Expand Up @@ -355,6 +355,7 @@ export default class Component extends Data {

/**
* Method for handling onAdd for all components
* @todo improve readability of this method
* @param {Object} evt
* @return {Object} Component
*/
Expand Down Expand Up @@ -397,10 +398,14 @@ export default class Component extends Data {

const onAddConditions = {
controls: () => {
const { controlData } = Controls.get(item.id)
const {
meta: { id: metaId },
} = controlData
controlData: {
meta: { id: metaId },
...elementData
},
} = Controls.get(item.id)

set(elementData, 'config.controlId', metaId)

const controlType = metaId.startsWith('layout-') ? metaId.replace(/^layout-/, '') : 'field'
const targets = {
Expand All @@ -424,7 +429,7 @@ export default class Component extends Data {
const depth = get(targets, `${this.name}.${controlType}`)
const action = depthMap.get(depth)()
dom.remove(item)
const component = action(controlData, newIndex)
const component = action(elementData, newIndex)

return component
},
Expand Down Expand Up @@ -501,10 +506,23 @@ export default class Component extends Data {
events.onRender && dom.onRender(this.dom, events.onRender)
}

/**
* Sets the configuration for the component. See src/demo/js/options/config.js for example
* @param {Object} config - Configuration object with possible structures:
* @param {Object} [config.all] - Global configuration applied to all components
* @param {Object} [config[controlId]] - Configuration specific to a control type
* @param {Object} [config[id]] - Configuration specific to a component instance
* @description Merges configurations in order of precedence:
* 1. Existing config (this.configVal)
* 2. Global config (all)
* 3. Control type specific config
* 4. Instance specific config
* The merged result is stored in this.configVal
*/
set config(config) {
const metaId = get(this.data, 'meta.id')
const allConfig = get(config, 'all')
const typeConfig = metaId && get(config, metaId)
const controlId = get(this.data, 'config.controlId')
const typeConfig = controlId && get(config, controlId)
const idConfig = get(config, this.id)
const mergedConfig = [allConfig, typeConfig, idConfig].reduce(
(acc, cur) => (cur ? merge(acc, cur) : acc),
Expand Down
31 changes: 18 additions & 13 deletions src/lib/js/components/controls/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import layoutControls from './layout/index.js'
import formControls from './form/index.js'
import htmlControls from './html/index.js'
import defaultOptions from './options.js'
import { get } from '../../common/utils/object.mjs'
import { get, set } from '../../common/utils/object.mjs'

const defaultElements = [...formControls, ...htmlControls, ...layoutControls]

Expand Down Expand Up @@ -126,17 +126,17 @@ export class Controls {
*/
groupConfig.content = elements.filter(control => {
const { controlData: field } = this.get(control.id)
const fieldId = field.meta.id || ''
const controlId = field.meta.id || ''
const filters = [
match(fieldId, this.options.disable.elements),
match(controlId, this.options.disable.elements),
field.meta.group === group.id,
!usedElementIds.includes(field.meta.id),
!usedElementIds.includes(controlId),
]

let shouldFilter = true
shouldFilter = filters.every(val => val === true)
if (shouldFilter) {
usedElementIds.push(fieldId)
usedElementIds.push(controlId)
}

return shouldFilter
Expand Down Expand Up @@ -349,24 +349,29 @@ export class Controls {
return element
}

layoutTypes = {
row: () => Stages.active.addChild(),
column: () => this.layoutTypes.row().addChild(),
field: controlData => this.layoutTypes.column().addChild(controlData),
}

/**
* Append an element to the stage
* @param {String} id of elements
*/
addElement = id => {
const controlData = get(this.get(id), 'controlData')

const {
meta: { group, id: metaId },
} = controlData
...elementData
} = get(this.get(id), 'controlData')

set(elementData, 'config.controlId', metaId)

const layoutTypes = {
row: () => Stages.active.addChild(),
column: () => layoutTypes.row().addChild(),
field: controlData => layoutTypes.column().addChild(controlData),
if (group === 'layout') {
return this.layoutTypes[metaId.replace('layout-', '')]()
}

return group !== 'layout' ? layoutTypes.field(controlData) : layoutTypes[metaId.replace('layout-', '')]()
return this.layoutTypes.field(elementData)
}

applyOptions = async (controlOptions = {}) => {
Expand Down
21 changes: 21 additions & 0 deletions src/lib/js/components/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,25 @@ export default class Data {
}
setCallbacks = {}
configVal = Object.create(null)

/**
* Parses the provided data argument. If the argument is a string, it attempts to parse it as JSON.
* If the parsing fails, it logs an error and returns an empty object.
* If the argument is not a string, it returns the argument as is.
*
* @param {string|Object} dataArg - The data to be parsed. Can be a JSON string or an object.
* @returns {Object} - The parsed object or the original object if the input was not a string.
*/
parseformData = (dataArg = Object.create(null)) => {
if (typeof dataArg === 'string') {
try {
return JSON.parse(dataArg)
} catch (e) {
console.error('Invalid JSON string provided:', e)
return Object.create(null)
}
}

return dataArg
}
}
11 changes: 6 additions & 5 deletions src/lib/js/components/fields/edit-panel.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ export default class EditPanel {
* @param {String} attr
* @param {String|Array} val
*/
addAttribute = (attr, val) => {
addAttribute = (attr, valArg) => {
let val = valArg
const safeAttr = slugify(attr)
const itemKey = `attrs.${safeAttr}`

Expand Down Expand Up @@ -157,16 +158,16 @@ export default class EditPanel {
* Add option to options panel
*/
addOption = () => {
const metaId = this.field.data.meta.id
const controlId = this.field.data.config.controlId
const fieldOptionData = this.field.get('options')
const type = metaId === 'select' ? 'option' : metaId
const type = controlId === 'select' ? 'option' : controlId
const newOptionLabel = i18n.get('newOptionLabel', { type }) || 'New Option'
const itemKey = `options.${this.data.length}`

const lastOptionData = fieldOptionData[fieldOptionData.length - 1]
const optionTemplate = fieldOptionData.length ? lastOptionData : {}
const itemData = { ...optionTemplate, label: newOptionLabel }
if (metaId !== 'button') {
if (controlId !== 'button') {
itemData.value = slugify(newOptionLabel)
}
const newOption = new EditPanelItem({
Expand Down
4 changes: 2 additions & 2 deletions src/lib/js/components/fields/field.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default class Field extends Component {
const hideLabel = !!this.get('config.hideLabel')

if (hideLabel) {
return
return null
}

const labelVal = this.get('config.editorLabel') || this.get('config.label')
Expand Down Expand Up @@ -308,7 +308,7 @@ export default class Field extends Component {
*/
fieldPreview() {
const prevData = clone(this.data)
const { action = {} } = controls.get(prevData.meta.id)
const { action = {} } = controls.get(prevData.config.controlId)
prevData.id = `prev-${this.id}`
prevData.action = action

Expand Down
19 changes: 18 additions & 1 deletion src/lib/js/components/fields/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import ComponentData from '../component-data.js'
import Field from './field.js'
import Controls from '../controls/index.js'
import { get } from '../../common/utils/object.mjs'
import { get, set } from '../../common/utils/object.mjs'

const DEFAULT_CONFIG = {
actionButtons: {
Expand Down Expand Up @@ -60,6 +60,23 @@ export class Fields extends ComponentData {
return acc
}, {})
}

load = (dataArg = Object.create(null)) => {
const allFieldData = this.parseformData(dataArg)
this.empty()

for (const [key, val] of Object.entries(allFieldData)) {
const { meta, ...data } = val
// meta object is only for controls, we want to migrate it out of field data
// we only need the control id to tie actions back to control definitons
if (meta?.id) {
set(data, 'config.controlId', meta?.id)
}
this.add(key, data)
}

return this.data
}
}

const fields = new Fields()
Expand Down
Loading