mirror of https://github.com/artf/grapesjs.git
committed by
GitHub
48 changed files with 3780 additions and 647 deletions
@ -1 +1 @@ |
|||||
./packages/core/README.md |
./packages/core/README.md |
||||
|
|||||
@ -0,0 +1,96 @@ |
|||||
|
import { DataSourcesEvents, DataSourceListener } from '../types'; |
||||
|
import { stringToPath } from '../../utils/mixins'; |
||||
|
import { Model } from '../../common'; |
||||
|
import EditorModel from '../../editor/model/Editor'; |
||||
|
import DataVariable, { DataVariableType } from './DataVariable'; |
||||
|
import { DataResolver } from '../types'; |
||||
|
import { DataCondition, DataConditionType } from './conditional_variables/DataCondition'; |
||||
|
import { DataCollectionVariableType } from './data_collection/constants'; |
||||
|
import DataCollectionVariable from './data_collection/DataCollectionVariable'; |
||||
|
|
||||
|
export interface DataResolverListenerProps { |
||||
|
em: EditorModel; |
||||
|
resolver: DataResolver; |
||||
|
onUpdate: (value: any) => void; |
||||
|
} |
||||
|
|
||||
|
export default class DataResolverListener { |
||||
|
private listeners: DataSourceListener[] = []; |
||||
|
private em: EditorModel; |
||||
|
private onUpdate: (value: any) => void; |
||||
|
private model = new Model(); |
||||
|
resolver: DataResolver; |
||||
|
|
||||
|
constructor(props: DataResolverListenerProps) { |
||||
|
this.em = props.em; |
||||
|
this.resolver = props.resolver; |
||||
|
this.onUpdate = props.onUpdate; |
||||
|
this.listenToResolver(); |
||||
|
} |
||||
|
|
||||
|
private onChange = () => { |
||||
|
const value = this.resolver.getDataValue(); |
||||
|
this.onUpdate(value); |
||||
|
}; |
||||
|
|
||||
|
listenToResolver() { |
||||
|
const { resolver, model } = this; |
||||
|
this.removeListeners(); |
||||
|
let listeners: DataSourceListener[] = []; |
||||
|
const type = resolver.attributes.type; |
||||
|
|
||||
|
switch (type) { |
||||
|
case DataCollectionVariableType: |
||||
|
listeners = this.listenToDataCollectionVariable(resolver as DataCollectionVariable); |
||||
|
break; |
||||
|
case DataVariableType: |
||||
|
listeners = this.listenToDataVariable(resolver as DataVariable); |
||||
|
break; |
||||
|
case DataConditionType: |
||||
|
listeners = this.listenToConditionalVariable(resolver as DataCondition); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
listeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange)); |
||||
|
this.listeners = listeners; |
||||
|
} |
||||
|
|
||||
|
private listenToConditionalVariable(dataVariable: DataCondition) { |
||||
|
const { em } = this; |
||||
|
const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { |
||||
|
return this.listenToDataVariable(new DataVariable(dataVariable, { em })); |
||||
|
}); |
||||
|
|
||||
|
return dataListeners; |
||||
|
} |
||||
|
|
||||
|
private listenToDataVariable(dataVariable: DataVariable) { |
||||
|
const { em } = this; |
||||
|
const dataListeners: DataSourceListener[] = []; |
||||
|
const { path } = dataVariable.attributes; |
||||
|
const normPath = stringToPath(path || '').join('.'); |
||||
|
const [ds, dr] = em.DataSources.fromPath(path!); |
||||
|
ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); |
||||
|
dr && dataListeners.push({ obj: dr, event: 'change' }); |
||||
|
dataListeners.push( |
||||
|
{ obj: dataVariable, event: 'change:path change:defaultValue' }, |
||||
|
{ obj: em.DataSources.all, event: 'add remove reset' }, |
||||
|
{ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, |
||||
|
); |
||||
|
|
||||
|
return dataListeners; |
||||
|
} |
||||
|
|
||||
|
private listenToDataCollectionVariable(dataVariable: DataCollectionVariable) { |
||||
|
return [{ obj: dataVariable, event: 'change:value' }]; |
||||
|
} |
||||
|
|
||||
|
private removeListeners() { |
||||
|
this.listeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange)); |
||||
|
this.listeners = []; |
||||
|
} |
||||
|
|
||||
|
destroy() { |
||||
|
this.removeListeners(); |
||||
|
} |
||||
|
} |
||||
@ -1,88 +0,0 @@ |
|||||
import { DataSourcesEvents, DataVariableListener } from '../types'; |
|
||||
import { stringToPath } from '../../utils/mixins'; |
|
||||
import { Model } from '../../common'; |
|
||||
import EditorModel from '../../editor/model/Editor'; |
|
||||
import DataVariable, { DataVariableType } from './DataVariable'; |
|
||||
import { DynamicValue } from '../types'; |
|
||||
import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition'; |
|
||||
import ComponentDataVariable from './ComponentDataVariable'; |
|
||||
|
|
||||
export interface DynamicVariableListenerManagerOptions { |
|
||||
em: EditorModel; |
|
||||
dataVariable: DynamicValue; |
|
||||
updateValueFromDataVariable: (value: any) => void; |
|
||||
} |
|
||||
|
|
||||
export default class DynamicVariableListenerManager { |
|
||||
private dataListeners: DataVariableListener[] = []; |
|
||||
private em: EditorModel; |
|
||||
dynamicVariable: DynamicValue; |
|
||||
private updateValueFromDynamicVariable: (value: any) => void; |
|
||||
private model = new Model(); |
|
||||
|
|
||||
constructor(options: DynamicVariableListenerManagerOptions) { |
|
||||
this.em = options.em; |
|
||||
this.dynamicVariable = options.dataVariable; |
|
||||
this.updateValueFromDynamicVariable = options.updateValueFromDataVariable; |
|
||||
|
|
||||
this.listenToDynamicVariable(); |
|
||||
} |
|
||||
|
|
||||
private onChange = () => { |
|
||||
const value = this.dynamicVariable.getDataValue(); |
|
||||
this.updateValueFromDynamicVariable(value); |
|
||||
}; |
|
||||
|
|
||||
listenToDynamicVariable() { |
|
||||
const { em, dynamicVariable } = this; |
|
||||
this.removeListeners(); |
|
||||
|
|
||||
// @ts-ignore
|
|
||||
const type = dynamicVariable.get('type'); |
|
||||
let dataListeners: DataVariableListener[] = []; |
|
||||
switch (type) { |
|
||||
case DataVariableType: |
|
||||
dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em); |
|
||||
break; |
|
||||
case ConditionalVariableType: |
|
||||
dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em); |
|
||||
break; |
|
||||
} |
|
||||
dataListeners.forEach((ls) => this.model.listenTo(ls.obj, ls.event, this.onChange)); |
|
||||
|
|
||||
this.dataListeners = dataListeners; |
|
||||
} |
|
||||
|
|
||||
private listenToConditionalVariable(dataVariable: DataCondition, em: EditorModel) { |
|
||||
const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { |
|
||||
return this.listenToDataVariable(new DataVariable(dataVariable, { em: this.em }), em); |
|
||||
}); |
|
||||
|
|
||||
return dataListeners; |
|
||||
} |
|
||||
|
|
||||
private listenToDataVariable(dataVariable: DataVariable | ComponentDataVariable, em: EditorModel) { |
|
||||
const dataListeners: DataVariableListener[] = []; |
|
||||
const { path } = dataVariable.attributes; |
|
||||
const normPath = stringToPath(path || '').join('.'); |
|
||||
const [ds, dr] = this.em.DataSources.fromPath(path); |
|
||||
ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' }); |
|
||||
dr && dataListeners.push({ obj: dr, event: 'change' }); |
|
||||
dataListeners.push( |
|
||||
{ obj: dataVariable, event: 'change:path change:defaultValue' }, |
|
||||
{ obj: em.DataSources.all, event: 'add remove reset' }, |
|
||||
{ obj: em, event: `${DataSourcesEvents.path}:${normPath}` }, |
|
||||
); |
|
||||
|
|
||||
return dataListeners; |
|
||||
} |
|
||||
|
|
||||
private removeListeners() { |
|
||||
this.dataListeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange)); |
|
||||
this.dataListeners = []; |
|
||||
} |
|
||||
|
|
||||
destroy() { |
|
||||
this.removeListeners(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,9 +0,0 @@ |
|||||
import DataVariable from './DataVariable'; |
|
||||
|
|
||||
export default class StyleDataVariable extends DataVariable { |
|
||||
defaults() { |
|
||||
return { |
|
||||
...super.defaults(), |
|
||||
}; |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,36 @@ |
|||||
|
import Component from '../../../dom_components/model/Component'; |
||||
|
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; |
||||
|
import { toLowerCase } from '../../../utils/mixins'; |
||||
|
import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition'; |
||||
|
|
||||
|
export default class ComponentDataCondition extends Component { |
||||
|
dataResolver: DataCondition; |
||||
|
|
||||
|
constructor(props: DataConditionProps, opt: ComponentOptions) { |
||||
|
const { condition, ifTrue, ifFalse } = props; |
||||
|
const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); |
||||
|
super( |
||||
|
{ |
||||
|
...props, |
||||
|
type: DataConditionType, |
||||
|
components: dataConditionInstance.getDataValue(), |
||||
|
}, |
||||
|
opt, |
||||
|
); |
||||
|
this.dataResolver = dataConditionInstance; |
||||
|
this.dataResolver.onValueChange = this.handleConditionChange.bind(this); |
||||
|
} |
||||
|
|
||||
|
private handleConditionChange() { |
||||
|
this.dataResolver.reevaluate(); |
||||
|
this.components(this.dataResolver.getDataValue()); |
||||
|
} |
||||
|
|
||||
|
static isComponent(el: HTMLElement) { |
||||
|
return toLowerCase(el.tagName) === DataConditionType; |
||||
|
} |
||||
|
|
||||
|
toJSON(): ComponentDefinition { |
||||
|
return this.dataResolver.toJSON(); |
||||
|
} |
||||
|
} |
||||
@ -1,46 +0,0 @@ |
|||||
import Component from '../../../dom_components/model/Component'; |
|
||||
import Components from '../../../dom_components/model/Components'; |
|
||||
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; |
|
||||
import { toLowerCase } from '../../../utils/mixins'; |
|
||||
import { DataCondition, ConditionalVariableType, ExpressionDefinition, LogicGroupDefinition } from './DataCondition'; |
|
||||
|
|
||||
type ConditionalComponentDefinition = { |
|
||||
condition: ExpressionDefinition | LogicGroupDefinition | boolean; |
|
||||
ifTrue: any; |
|
||||
ifFalse: any; |
|
||||
}; |
|
||||
|
|
||||
export default class ComponentConditionalVariable extends Component { |
|
||||
dataCondition: DataCondition; |
|
||||
componentDefinition: ConditionalComponentDefinition; |
|
||||
|
|
||||
constructor(componentDefinition: ConditionalComponentDefinition, opt: ComponentOptions) { |
|
||||
const { condition, ifTrue, ifFalse } = componentDefinition; |
|
||||
const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); |
|
||||
const initialComponentsProps = dataConditionInstance.getDataValue(); |
|
||||
const conditionalCmptDef = { |
|
||||
type: ConditionalVariableType, |
|
||||
components: initialComponentsProps, |
|
||||
}; |
|
||||
super(conditionalCmptDef, opt); |
|
||||
|
|
||||
this.componentDefinition = componentDefinition; |
|
||||
this.dataCondition = dataConditionInstance; |
|
||||
this.dataCondition.onValueChange = this.handleConditionChange.bind(this); |
|
||||
} |
|
||||
|
|
||||
private handleConditionChange() { |
|
||||
this.dataCondition.reevaluate(); |
|
||||
const updatedComponents = this.dataCondition.getDataValue(); |
|
||||
this.components().reset(); |
|
||||
this.components().add(updatedComponents); |
|
||||
} |
|
||||
|
|
||||
static isComponent(el: HTMLElement) { |
|
||||
return toLowerCase(el.tagName) === ConditionalVariableType; |
|
||||
} |
|
||||
|
|
||||
toJSON(): ComponentDefinition { |
|
||||
return this.dataCondition.toJSON(); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,284 @@ |
|||||
|
import { isArray } from 'underscore'; |
||||
|
import { ObjectAny } from '../../../common'; |
||||
|
import Component from '../../../dom_components/model/Component'; |
||||
|
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; |
||||
|
import EditorModel from '../../../editor/model/Editor'; |
||||
|
import { isObject, serialize, toLowerCase } from '../../../utils/mixins'; |
||||
|
import DataResolverListener from '../DataResolverListener'; |
||||
|
import DataSource from '../DataSource'; |
||||
|
import DataVariable, { DataVariableProps, DataVariableType } from '../DataVariable'; |
||||
|
import { isDataVariable } from '../utils'; |
||||
|
import { DataCollectionType, keyCollectionDefinition, keyCollectionsStateMap, keyIsCollectionItem } from './constants'; |
||||
|
import { |
||||
|
ComponentDataCollectionProps, |
||||
|
DataCollectionConfig, |
||||
|
DataCollectionDataSource, |
||||
|
DataCollectionProps, |
||||
|
DataCollectionState, |
||||
|
DataCollectionStateMap, |
||||
|
} from './types'; |
||||
|
import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers'; |
||||
|
|
||||
|
export default class ComponentDataCollection extends Component { |
||||
|
constructor(props: ComponentDataCollectionProps, opt: ComponentOptions) { |
||||
|
const collectionDef = props[keyCollectionDefinition]; |
||||
|
// If we are cloning, leave setting the collection items to the main symbol collection
|
||||
|
if (opt.forCloning) { |
||||
|
return super(props as any, opt) as unknown as ComponentDataCollection; |
||||
|
} |
||||
|
|
||||
|
const em = opt.em; |
||||
|
const newProps = { ...props, components: undefined, droppable: false } as any; |
||||
|
const cmp: ComponentDataCollection = super(newProps, opt) as unknown as ComponentDataCollection; |
||||
|
|
||||
|
if (!collectionDef) { |
||||
|
em.logError('missing collection definition'); |
||||
|
return cmp; |
||||
|
} |
||||
|
|
||||
|
const parentCollectionStateMap = (props[keyCollectionsStateMap] || {}) as DataCollectionStateMap; |
||||
|
const components: Component[] = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt); |
||||
|
cmp.components(components, opt); |
||||
|
|
||||
|
if (isDataVariable(this.collectionDataSource)) { |
||||
|
this.watchDataSource(parentCollectionStateMap, opt); |
||||
|
} |
||||
|
|
||||
|
return cmp; |
||||
|
} |
||||
|
|
||||
|
get collectionConfig() { |
||||
|
return this.get(keyCollectionDefinition).collectionConfig as DataCollectionConfig; |
||||
|
} |
||||
|
|
||||
|
get collectionDataSource() { |
||||
|
return this.collectionConfig.dataSource; |
||||
|
} |
||||
|
|
||||
|
toJSON(opts?: ObjectAny) { |
||||
|
const json = super.toJSON.call(this, opts) as ComponentDataCollectionProps; |
||||
|
json[keyCollectionDefinition].componentDef = this.getComponentDef(); |
||||
|
delete json.components; |
||||
|
delete json.droppable; |
||||
|
return json; |
||||
|
} |
||||
|
|
||||
|
private getComponentDef() { |
||||
|
const firstChild = this.components().at(0); |
||||
|
const firstChildJSON = firstChild ? serialize(firstChild) : this.get(keyCollectionDefinition).componentDef; |
||||
|
delete firstChildJSON?.draggable; |
||||
|
return firstChildJSON; |
||||
|
} |
||||
|
|
||||
|
private watchDataSource(parentCollectionStateMap: DataCollectionStateMap, opt: ComponentOptions) { |
||||
|
const { em } = this; |
||||
|
const path = this.collectionDataSource?.path; |
||||
|
if (!path) return; |
||||
|
|
||||
|
new DataResolverListener({ |
||||
|
em, |
||||
|
resolver: new DataVariable({ type: DataVariableType, path }, { em }), |
||||
|
onUpdate: () => { |
||||
|
const collectionDef = { ...this.get(keyCollectionDefinition), componentDef: this.getComponentDef() }; |
||||
|
const collectionItems = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt); |
||||
|
this.components().reset(collectionItems, updateFromWatcher as any); |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
static isComponent(el: HTMLElement) { |
||||
|
return toLowerCase(el.tagName) === DataCollectionType; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function getCollectionItems( |
||||
|
em: EditorModel, |
||||
|
collectionDef: DataCollectionProps, |
||||
|
parentCollectionStateMap: DataCollectionStateMap, |
||||
|
opt: ComponentOptions, |
||||
|
) { |
||||
|
const { componentDef, collectionConfig } = collectionDef; |
||||
|
const result = validateCollectionConfig(collectionConfig, componentDef, em); |
||||
|
if (!result) { |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
const components: Component[] = []; |
||||
|
const collectionId = collectionConfig.collectionId; |
||||
|
const items = getDataSourceItems(collectionConfig.dataSource, em); |
||||
|
const startIndex = Math.max(0, collectionConfig.startIndex || 0); |
||||
|
const endIndex = Math.min( |
||||
|
items.length - 1, |
||||
|
collectionConfig.endIndex !== undefined ? collectionConfig.endIndex : Number.MAX_VALUE, |
||||
|
); |
||||
|
const totalItems = endIndex - startIndex + 1; |
||||
|
let symbolMain: Component; |
||||
|
|
||||
|
for (let index = startIndex; index <= endIndex; index++) { |
||||
|
const item = items[index]; |
||||
|
const collectionState: DataCollectionState = { |
||||
|
collectionId, |
||||
|
currentIndex: index, |
||||
|
currentItem: item, |
||||
|
startIndex: startIndex, |
||||
|
endIndex: endIndex, |
||||
|
totalItems: totalItems, |
||||
|
remainingItems: totalItems - (index + 1), |
||||
|
}; |
||||
|
|
||||
|
if (parentCollectionStateMap[collectionId]) { |
||||
|
em.logError( |
||||
|
`The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`, |
||||
|
); |
||||
|
return []; |
||||
|
} |
||||
|
|
||||
|
const collectionsStateMap: DataCollectionStateMap = { |
||||
|
...parentCollectionStateMap, |
||||
|
[collectionId]: collectionState, |
||||
|
}; |
||||
|
|
||||
|
if (index === startIndex) { |
||||
|
const componentType = (componentDef?.type as string) || 'default'; |
||||
|
let type = em.Components.getType(componentType) || em.Components.getType('default'); |
||||
|
const Model = type.model; |
||||
|
symbolMain = new Model({ ...serialize(componentDef), draggable: false }, opt); |
||||
|
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(symbolMain); |
||||
|
} |
||||
|
|
||||
|
const instance = symbolMain!.clone({ symbol: true }); |
||||
|
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance); |
||||
|
|
||||
|
components.push(instance); |
||||
|
} |
||||
|
|
||||
|
return components; |
||||
|
} |
||||
|
|
||||
|
function setCollectionStateMapAndPropagate( |
||||
|
collectionsStateMap: DataCollectionStateMap, |
||||
|
collectionId: string | undefined, |
||||
|
) { |
||||
|
return (cmp: Component) => { |
||||
|
setCollectionStateMap(collectionsStateMap)(cmp); |
||||
|
|
||||
|
const addListener = (component: Component) => { |
||||
|
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(component); |
||||
|
}; |
||||
|
|
||||
|
const listenerKey = `_hasAddListener${collectionId ? `_${collectionId}` : ''}`; |
||||
|
const cmps = cmp.components(); |
||||
|
|
||||
|
// Add the 'add' listener if not already in the listeners array
|
||||
|
if (!cmp.collectionStateListeners.includes(listenerKey)) { |
||||
|
cmp.listenTo(cmps, 'add', addListener); |
||||
|
cmp.collectionStateListeners.push(listenerKey); |
||||
|
|
||||
|
const removeListener = (component: Component) => { |
||||
|
component.stopListening(component.components(), 'add', addListener); |
||||
|
component.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); |
||||
|
const index = component.collectionStateListeners.indexOf(listenerKey); |
||||
|
if (index > -1) { |
||||
|
component.collectionStateListeners.splice(index, 1); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
cmp.listenTo(cmps, 'remove', removeListener); |
||||
|
} |
||||
|
|
||||
|
cmps?.toArray().forEach((component: Component) => { |
||||
|
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(component); |
||||
|
}); |
||||
|
|
||||
|
cmp.on(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function handleCollectionStateMapChange(this: Component) { |
||||
|
const updatedCollectionsStateMap = this.get(keyCollectionsStateMap); |
||||
|
this.components() |
||||
|
?.toArray() |
||||
|
.forEach((component: Component) => { |
||||
|
setCollectionStateMap(updatedCollectionsStateMap)(component); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function logErrorIfMissing(property: any, propertyPath: string, em: EditorModel) { |
||||
|
if (!property) { |
||||
|
em.logError(`The "${propertyPath}" property is required in the collection definition.`); |
||||
|
return false; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
function validateCollectionConfig( |
||||
|
collectionConfig: DataCollectionConfig, |
||||
|
componentDef: ComponentDefinition, |
||||
|
em: EditorModel, |
||||
|
) { |
||||
|
const validations = [ |
||||
|
{ property: collectionConfig, propertyPath: 'collectionConfig' }, |
||||
|
{ property: componentDef, propertyPath: 'componentDef' }, |
||||
|
{ property: collectionConfig?.collectionId, propertyPath: 'collectionConfig.collectionId' }, |
||||
|
{ property: collectionConfig?.dataSource, propertyPath: 'collectionConfig.dataSource' }, |
||||
|
]; |
||||
|
|
||||
|
for (const { property, propertyPath } of validations) { |
||||
|
if (!logErrorIfMissing(property, propertyPath, em)) { |
||||
|
return []; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
function setCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { |
||||
|
return (cmp: Component) => { |
||||
|
cmp.set(keyIsCollectionItem, true); |
||||
|
const updatedCollectionStateMap = { |
||||
|
...cmp.get(keyCollectionsStateMap), |
||||
|
...collectionsStateMap, |
||||
|
}; |
||||
|
cmp.set(keyCollectionsStateMap, updatedCollectionStateMap); |
||||
|
cmp.dataResolverWatchers.updateCollectionStateMap(updatedCollectionStateMap); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function getDataSourceItems(dataSource: DataCollectionDataSource, em: EditorModel) { |
||||
|
let items: DataVariableProps[] = []; |
||||
|
|
||||
|
switch (true) { |
||||
|
case isArray(dataSource): |
||||
|
items = dataSource; |
||||
|
break; |
||||
|
case isObject(dataSource) && dataSource instanceof DataSource: { |
||||
|
const id = dataSource.get('id')!; |
||||
|
items = listDataSourceVariables(id, em); |
||||
|
break; |
||||
|
} |
||||
|
case isDataVariable(dataSource): { |
||||
|
const isDataSourceId = dataSource.path.split('.').length === 1; |
||||
|
if (isDataSourceId) { |
||||
|
const id = dataSource.path; |
||||
|
items = listDataSourceVariables(id, em); |
||||
|
} else { |
||||
|
// Path points to a record in the data source
|
||||
|
items = em.DataSources.getValue(dataSource.path, []); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
default: |
||||
|
} |
||||
|
|
||||
|
return items; |
||||
|
} |
||||
|
|
||||
|
function listDataSourceVariables(dataSource_id: string, em: EditorModel): DataVariableProps[] { |
||||
|
const records = em.DataSources.getValue(dataSource_id, []); |
||||
|
const keys = Object.keys(records); |
||||
|
|
||||
|
return keys.map((key) => ({ |
||||
|
type: DataVariableType, |
||||
|
path: dataSource_id + '.' + key, |
||||
|
})); |
||||
|
} |
||||
@ -0,0 +1,53 @@ |
|||||
|
import Component from '../../../dom_components/model/Component'; |
||||
|
import { ComponentOptions } from '../../../dom_components/model/types'; |
||||
|
import { toLowerCase } from '../../../utils/mixins'; |
||||
|
import DataCollectionVariable from './DataCollectionVariable'; |
||||
|
import { DataCollectionVariableType, keyCollectionsStateMap } from './constants'; |
||||
|
import { ComponentDataCollectionVariableProps, DataCollectionStateMap } from './types'; |
||||
|
|
||||
|
export default class ComponentDataCollectionVariable extends Component { |
||||
|
dataResolver: DataCollectionVariable; |
||||
|
|
||||
|
get defaults() { |
||||
|
// @ts-expect-error
|
||||
|
const componentDefaults = super.defaults; |
||||
|
|
||||
|
return { |
||||
|
...componentDefaults, |
||||
|
type: DataCollectionVariableType, |
||||
|
collectionId: undefined, |
||||
|
variableType: undefined, |
||||
|
path: undefined, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
constructor(props: ComponentDataCollectionVariableProps, opt: ComponentOptions) { |
||||
|
super(props, opt); |
||||
|
const { type, variableType, path, collectionId } = props; |
||||
|
this.dataResolver = new DataCollectionVariable( |
||||
|
{ type, variableType, path, collectionId }, |
||||
|
{ |
||||
|
...opt, |
||||
|
collectionsStateMap: this.get(keyCollectionsStateMap), |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
this.listenTo(this, `change:${keyCollectionsStateMap}`, this.handleCollectionsMapStateUpdate); |
||||
|
} |
||||
|
|
||||
|
private handleCollectionsMapStateUpdate(m: any, v: DataCollectionStateMap, opts = {}) { |
||||
|
this.dataResolver.updateCollectionsStateMap(v); |
||||
|
} |
||||
|
|
||||
|
getDataValue() { |
||||
|
return this.dataResolver.getDataValue(); |
||||
|
} |
||||
|
|
||||
|
getInnerHTML() { |
||||
|
return this.getDataValue(); |
||||
|
} |
||||
|
|
||||
|
static isComponent(el: HTMLElement) { |
||||
|
return toLowerCase(el.tagName) === DataCollectionVariableType; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,158 @@ |
|||||
|
import { DataCollectionVariableProps } from './types'; |
||||
|
import { Model } from '../../../common'; |
||||
|
import EditorModel from '../../../editor/model/Editor'; |
||||
|
import DataVariable, { DataVariableType } from '../DataVariable'; |
||||
|
import { DataCollectionVariableType } from './constants'; |
||||
|
import { DataCollectionState, DataCollectionStateMap } from './types'; |
||||
|
import DataResolverListener from '../DataResolverListener'; |
||||
|
|
||||
|
interface DataCollectionVariablePropsDefined extends DataCollectionVariableProps { |
||||
|
value?: any; |
||||
|
} |
||||
|
|
||||
|
export default class DataCollectionVariable extends Model<DataCollectionVariablePropsDefined> { |
||||
|
em: EditorModel; |
||||
|
collectionsStateMap?: DataCollectionStateMap; |
||||
|
dataVariable?: DataVariable; |
||||
|
resolverListener?: DataResolverListener; |
||||
|
|
||||
|
defaults(): Partial<DataCollectionVariablePropsDefined> { |
||||
|
return { |
||||
|
type: DataCollectionVariableType, |
||||
|
collectionId: undefined, |
||||
|
variableType: undefined, |
||||
|
path: undefined, |
||||
|
value: undefined, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
constructor( |
||||
|
props: DataCollectionVariablePropsDefined, |
||||
|
options: { |
||||
|
em: EditorModel; |
||||
|
collectionsStateMap?: DataCollectionStateMap; |
||||
|
}, |
||||
|
) { |
||||
|
super(props, options); |
||||
|
this.em = options.em; |
||||
|
this.collectionsStateMap = options.collectionsStateMap; |
||||
|
this.updateDataVariable(); |
||||
|
} |
||||
|
|
||||
|
hasDynamicValue() { |
||||
|
return !!this.dataVariable; |
||||
|
} |
||||
|
|
||||
|
getDataValue() { |
||||
|
const { resolvedValue } = this.updateDataVariable(); |
||||
|
|
||||
|
if (resolvedValue?.type === DataVariableType) { |
||||
|
return this.dataVariable!.getDataValue(); |
||||
|
} |
||||
|
|
||||
|
return resolvedValue; |
||||
|
} |
||||
|
|
||||
|
private updateDataVariable() { |
||||
|
if (!this.collectionsStateMap) return { resolvedValue: undefined }; |
||||
|
|
||||
|
const resolvedValue = resolveCollectionVariable( |
||||
|
this.attributes as DataCollectionVariableProps, |
||||
|
this.collectionsStateMap, |
||||
|
this.em, |
||||
|
); |
||||
|
|
||||
|
let dataVariable; |
||||
|
if (resolvedValue?.type === DataVariableType) { |
||||
|
dataVariable = new DataVariable(resolvedValue, { em: this.em }); |
||||
|
this.dataVariable = dataVariable; |
||||
|
|
||||
|
this.resolverListener?.destroy(); |
||||
|
this.resolverListener = new DataResolverListener({ |
||||
|
em: this.em, |
||||
|
resolver: dataVariable, |
||||
|
onUpdate: () => { |
||||
|
this.set('value', this.dataVariable?.getDataValue()); |
||||
|
}, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
this.set('value', resolvedValue); |
||||
|
return { resolvedValue, dataVariable }; |
||||
|
} |
||||
|
|
||||
|
updateCollectionsStateMap(collectionsStateMap: DataCollectionStateMap) { |
||||
|
this.collectionsStateMap = collectionsStateMap; |
||||
|
this.updateDataVariable(); |
||||
|
} |
||||
|
|
||||
|
destroy() { |
||||
|
this.resolverListener?.destroy(); |
||||
|
this.dataVariable?.destroy(); |
||||
|
|
||||
|
return super.destroy(); |
||||
|
} |
||||
|
|
||||
|
toJSON(options?: any) { |
||||
|
const json = super.toJSON(options); |
||||
|
delete json.value; |
||||
|
!json.collectionId && delete json.collectionId; |
||||
|
|
||||
|
return json; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function resolveCollectionVariable( |
||||
|
collectionVariableDefinition: DataCollectionVariableProps, |
||||
|
collectionsStateMap: DataCollectionStateMap, |
||||
|
em: EditorModel, |
||||
|
) { |
||||
|
const { collectionId, variableType, path } = collectionVariableDefinition; |
||||
|
if (!collectionsStateMap) return; |
||||
|
|
||||
|
const collectionItem = collectionsStateMap[collectionId]; |
||||
|
|
||||
|
if (!collectionItem) { |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
if (!variableType) { |
||||
|
em.logError(`Missing collection variable type for collection: ${collectionId}`); |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
if (variableType === 'currentItem') { |
||||
|
return resolveCurrentItem(collectionItem, path, collectionId, em); |
||||
|
} |
||||
|
|
||||
|
return collectionItem[variableType]; |
||||
|
} |
||||
|
|
||||
|
function resolveCurrentItem( |
||||
|
collectionItem: DataCollectionState, |
||||
|
path: string | undefined, |
||||
|
collectionId: string, |
||||
|
em: EditorModel, |
||||
|
) { |
||||
|
const currentItem = collectionItem.currentItem; |
||||
|
|
||||
|
if (!currentItem) { |
||||
|
em.logError(`Current item is missing for collection: ${collectionId}`); |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
if (currentItem.type === DataVariableType) { |
||||
|
const resolvedPath = currentItem.path ? `${currentItem.path}.${path}` : path; |
||||
|
return { |
||||
|
...currentItem, |
||||
|
path: resolvedPath, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
if (path && !(currentItem as any)[path]) { |
||||
|
em.logError(`Path not found in current item: ${path} for collection: ${collectionId}`); |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
return path ? (currentItem as any)[path] : currentItem; |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
export const DataCollectionType = 'data-collection'; |
||||
|
export const DataCollectionVariableType = 'data-collection-variable'; |
||||
|
export const keyCollectionDefinition = 'collectionDef'; |
||||
|
export const keyIsCollectionItem = '__is_data_collection_item'; |
||||
|
export const keyCollectionsStateMap = '__collections_state_map'; |
||||
@ -0,0 +1,57 @@ |
|||||
|
import { DataCollectionType, DataCollectionVariableType, keyCollectionDefinition } from './constants'; |
||||
|
import { ComponentDefinition, ComponentProperties } from '../../../dom_components/model/types'; |
||||
|
import { DataVariableProps } from '../DataVariable'; |
||||
|
|
||||
|
export type DataCollectionDataSource = DataVariableProps | DataCollectionVariableProps; |
||||
|
|
||||
|
export interface DataCollectionConfig { |
||||
|
collectionId: string; |
||||
|
startIndex?: number; |
||||
|
endIndex?: number; |
||||
|
dataSource: DataCollectionDataSource; |
||||
|
} |
||||
|
|
||||
|
export enum DataCollectionStateVariableType { |
||||
|
currentIndex = 'currentIndex', |
||||
|
startIndex = 'startIndex', |
||||
|
currentItem = 'currentItem', |
||||
|
endIndex = 'endIndex', |
||||
|
collectionId = 'collectionId', |
||||
|
totalItems = 'totalItems', |
||||
|
remainingItems = 'remainingItems', |
||||
|
} |
||||
|
|
||||
|
export interface DataCollectionState { |
||||
|
[DataCollectionStateVariableType.currentIndex]: number; |
||||
|
[DataCollectionStateVariableType.startIndex]: number; |
||||
|
[DataCollectionStateVariableType.currentItem]: DataVariableProps; |
||||
|
[DataCollectionStateVariableType.endIndex]: number; |
||||
|
[DataCollectionStateVariableType.collectionId]: string; |
||||
|
[DataCollectionStateVariableType.totalItems]: number; |
||||
|
[DataCollectionStateVariableType.remainingItems]: number; |
||||
|
} |
||||
|
|
||||
|
export interface DataCollectionStateMap { |
||||
|
[key: string]: DataCollectionState; |
||||
|
} |
||||
|
|
||||
|
export interface ComponentDataCollectionProps extends ComponentDefinition { |
||||
|
[keyCollectionDefinition]: DataCollectionProps; |
||||
|
} |
||||
|
|
||||
|
export interface ComponentDataCollectionVariableProps |
||||
|
extends DataCollectionVariableProps, |
||||
|
Omit<ComponentProperties, 'type'> {} |
||||
|
|
||||
|
export interface DataCollectionProps { |
||||
|
type: typeof DataCollectionType; |
||||
|
collectionConfig: DataCollectionConfig; |
||||
|
componentDef: ComponentDefinition; |
||||
|
} |
||||
|
|
||||
|
export interface DataCollectionVariableProps { |
||||
|
type: typeof DataCollectionVariableType; |
||||
|
variableType: DataCollectionStateVariableType; |
||||
|
collectionId: string; |
||||
|
path?: string; |
||||
|
} |
||||
@ -1,50 +1,68 @@ |
|||||
import EditorModel from '../../editor/model/Editor'; |
import EditorModel from '../../editor/model/Editor'; |
||||
import { DynamicValue, DynamicValueDefinition } from '../types'; |
import { DataResolver, DataResolverProps } from '../types'; |
||||
import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition'; |
import { DataCollectionStateMap } from './data_collection/types'; |
||||
import DataVariable, { DataVariableType } from './DataVariable'; |
import DataCollectionVariable from './data_collection/DataCollectionVariable'; |
||||
|
import { DataCollectionVariableType } from './data_collection/constants'; |
||||
|
import { DataConditionType, DataCondition } from './conditional_variables/DataCondition'; |
||||
|
import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable'; |
||||
|
|
||||
export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition { |
export function isDataResolverProps(value: any): value is DataResolverProps { |
||||
return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value?.type); |
return ( |
||||
|
typeof value === 'object' && [DataVariableType, DataConditionType, DataCollectionVariableType].includes(value?.type) |
||||
|
); |
||||
} |
} |
||||
|
|
||||
export function isDynamicValue(value: any): value is DynamicValue { |
export function isDataResolver(value: any): value is DataResolver { |
||||
return value instanceof DataVariable || value instanceof DataCondition; |
return value instanceof DataVariable || value instanceof DataCondition; |
||||
} |
} |
||||
|
|
||||
export function isDataVariable(variable: any) { |
export function isDataVariable(variable: any): variable is DataVariableProps { |
||||
return variable?.type === DataVariableType; |
return variable?.type === DataVariableType; |
||||
} |
} |
||||
|
|
||||
export function isDataCondition(variable: any) { |
export function isDataCondition(variable: any) { |
||||
return variable?.type === ConditionalVariableType; |
return variable?.type === DataConditionType; |
||||
} |
} |
||||
|
|
||||
export function evaluateVariable(variable: any, em: EditorModel) { |
export function evaluateVariable(variable: any, em: EditorModel) { |
||||
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; |
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; |
||||
} |
} |
||||
|
|
||||
export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, em: EditorModel): DynamicValue { |
export function getDataResolverInstance( |
||||
const dynamicType = valueDefinition.type; |
resolverProps: DataResolverProps, |
||||
let dynamicVariable: DynamicValue; |
options: { em: EditorModel; collectionsStateMap?: DataCollectionStateMap }, |
||||
|
): DataResolver { |
||||
|
const { type } = resolverProps; |
||||
|
let resolver: DataResolver; |
||||
|
|
||||
switch (dynamicType) { |
switch (type) { |
||||
case DataVariableType: |
case DataVariableType: |
||||
dynamicVariable = new DataVariable(valueDefinition, { em: em }); |
resolver = new DataVariable(resolverProps, options); |
||||
break; |
break; |
||||
case ConditionalVariableType: { |
case DataConditionType: { |
||||
const { condition, ifTrue, ifFalse } = valueDefinition; |
const { condition, ifTrue, ifFalse } = resolverProps; |
||||
dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: em }); |
resolver = new DataCondition(condition, ifTrue, ifFalse, options); |
||||
|
break; |
||||
|
} |
||||
|
case DataCollectionVariableType: { |
||||
|
resolver = new DataCollectionVariable(resolverProps, options); |
||||
break; |
break; |
||||
} |
} |
||||
default: |
default: |
||||
throw new Error(`Unsupported dynamic type: ${dynamicType}`); |
throw new Error(`Unsupported dynamic type: ${type}`); |
||||
} |
} |
||||
|
|
||||
return dynamicVariable; |
return resolver; |
||||
} |
} |
||||
|
|
||||
export function evaluateDynamicValueDefinition(valueDefinition: DynamicValueDefinition, em: EditorModel) { |
export function getDataResolverInstanceValue( |
||||
const dynamicVariable = getDynamicValueInstance(valueDefinition, em); |
resolverProps: DataResolverProps, |
||||
|
options: { |
||||
|
em: EditorModel; |
||||
|
collectionsStateMap?: DataCollectionStateMap; |
||||
|
}, |
||||
|
) { |
||||
|
const resolver = getDataResolverInstance(resolverProps, options); |
||||
|
|
||||
return { variable: dynamicVariable, value: dynamicVariable.getDataValue() }; |
return { resolver, value: resolver.getDataValue() }; |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,21 @@ |
|||||
|
import ComponentView from '../../dom_components/view/ComponentView'; |
||||
|
import DataResolverListener from '../model/DataResolverListener'; |
||||
|
import ComponentDataCollectionVariable from '../model/data_collection/ComponentDataCollectionVariable'; |
||||
|
|
||||
|
export default class ComponentDataCollectionVariableView extends ComponentView<ComponentDataCollectionVariable> { |
||||
|
dataResolverListener?: DataResolverListener; |
||||
|
|
||||
|
initialize(opt = {}) { |
||||
|
super.initialize(opt); |
||||
|
this.dataResolverListener = new DataResolverListener({ |
||||
|
em: this.em!, |
||||
|
resolver: this.model.dataResolver, |
||||
|
onUpdate: this.postRender.bind(this), |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
postRender() { |
||||
|
this.el.innerHTML = this.model.getDataValue(); |
||||
|
super.postRender(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,4 @@ |
|||||
|
import ComponentView from '../../dom_components/view/ComponentView'; |
||||
|
import ComponentDataCollection from '../model/data_collection/ComponentDataCollection'; |
||||
|
|
||||
|
export default class ComponentDataCollectionView extends ComponentView<ComponentDataCollection> {} |
||||
@ -0,0 +1,4 @@ |
|||||
|
import ComponentView from '../../dom_components/view/ComponentView'; |
||||
|
import ComponentDataCondition from '../model/conditional_variables/ComponentDataCondition'; |
||||
|
|
||||
|
export default class ComponentDataConditionView extends ComponentView<ComponentDataCondition> {} |
||||
@ -1,23 +1,26 @@ |
|||||
import ComponentView from '../../dom_components/view/ComponentView'; |
import ComponentView from '../../dom_components/view/ComponentView'; |
||||
import ComponentDataVariable from '../model/ComponentDataVariable'; |
import ComponentDataVariable from '../model/ComponentDataVariable'; |
||||
import DynamicVariableListenerManager from '../model/DataVariableListenerManager'; |
import DataResolverListener from '../model/DataResolverListener'; |
||||
|
|
||||
export default class ComponentDataVariableView extends ComponentView<ComponentDataVariable> { |
export default class ComponentDataVariableView extends ComponentView<ComponentDataVariable> { |
||||
dynamicVariableListener?: DynamicVariableListenerManager; |
dataResolverListener!: DataResolverListener; |
||||
|
|
||||
initialize(opt = {}) { |
initialize(opt = {}) { |
||||
super.initialize(opt); |
super.initialize(opt); |
||||
this.dynamicVariableListener = new DynamicVariableListenerManager({ |
this.dataResolverListener = new DataResolverListener({ |
||||
em: this.em!, |
em: this.em, |
||||
dataVariable: this.model, |
resolver: this.model.dataResolver, |
||||
updateValueFromDataVariable: () => this.postRender(), |
onUpdate: () => this.postRender(), |
||||
}); |
}); |
||||
} |
} |
||||
|
|
||||
|
remove() { |
||||
|
this.dataResolverListener.destroy(); |
||||
|
return super.remove(); |
||||
|
} |
||||
|
|
||||
postRender() { |
postRender() { |
||||
const { model, el, em } = this; |
this.el.innerHTML = this.model.getDataValue(); |
||||
const { path, defaultValue } = model.attributes; |
|
||||
el.innerHTML = em.DataSources.getValue(path, defaultValue); |
|
||||
super.postRender(); |
super.postRender(); |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -1,4 +0,0 @@ |
|||||
import ComponentView from '../../dom_components/view/ComponentView'; |
|
||||
import ConditionalComponent from '../model/conditional_variables/ConditionalComponent'; |
|
||||
|
|
||||
export default class ConditionalComponentView extends ComponentView<ConditionalComponent> {} |
|
||||
@ -0,0 +1,116 @@ |
|||||
|
import { ObjectAny } from '../../common'; |
||||
|
import { |
||||
|
DataCollectionVariableType, |
||||
|
keyCollectionsStateMap, |
||||
|
keyIsCollectionItem, |
||||
|
} from '../../data_sources/model/data_collection/constants'; |
||||
|
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; |
||||
|
import Component from './Component'; |
||||
|
import { |
||||
|
ComponentResolverWatcher, |
||||
|
ComponentResolverWatcherOptions, |
||||
|
DynamicWatchersOptions, |
||||
|
} from './ComponentResolverWatcher'; |
||||
|
import { getSymbolsToUpdate } from './SymbolUtils'; |
||||
|
|
||||
|
export const updateFromWatcher = { fromDataSource: true, avoidStore: true }; |
||||
|
|
||||
|
export class ComponentDataResolverWatchers { |
||||
|
private propertyWatcher: ComponentResolverWatcher; |
||||
|
private attributeWatcher: ComponentResolverWatcher; |
||||
|
|
||||
|
constructor( |
||||
|
private component: Component | undefined, |
||||
|
options: ComponentResolverWatcherOptions, |
||||
|
) { |
||||
|
this.propertyWatcher = new ComponentResolverWatcher(component, this.onPropertyUpdate, options); |
||||
|
this.attributeWatcher = new ComponentResolverWatcher(component, this.onAttributeUpdate, options); |
||||
|
} |
||||
|
|
||||
|
private onPropertyUpdate(component: Component | undefined, key: string, value: any) { |
||||
|
component?.set(key, value, updateFromWatcher); |
||||
|
} |
||||
|
|
||||
|
private onAttributeUpdate(component: Component | undefined, key: string, value: any) { |
||||
|
component?.addAttributes({ [key]: value }, updateFromWatcher); |
||||
|
} |
||||
|
|
||||
|
bindComponent(component: Component) { |
||||
|
this.component = component; |
||||
|
this.propertyWatcher.bindComponent(component); |
||||
|
this.attributeWatcher.bindComponent(component); |
||||
|
this.updateSymbolOverride(); |
||||
|
} |
||||
|
|
||||
|
updateCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { |
||||
|
this.propertyWatcher.updateCollectionStateMap(collectionsStateMap); |
||||
|
this.attributeWatcher.updateCollectionStateMap(collectionsStateMap); |
||||
|
} |
||||
|
|
||||
|
addProps(props: ObjectAny, options: DynamicWatchersOptions = {}) { |
||||
|
const excludedFromEvaluation = ['components']; |
||||
|
|
||||
|
const evaluatedProps = Object.fromEntries( |
||||
|
Object.entries(props).map(([key, value]) => |
||||
|
excludedFromEvaluation.includes(key) |
||||
|
? [key, value] // Return excluded keys as they are
|
||||
|
: [key, this.propertyWatcher.addDynamicValues({ [key]: value }, options)[key]], |
||||
|
), |
||||
|
); |
||||
|
|
||||
|
if (props.attributes) { |
||||
|
const evaluatedAttributes = this.attributeWatcher.setDynamicValues(props.attributes, options); |
||||
|
evaluatedProps['attributes'] = evaluatedAttributes; |
||||
|
} |
||||
|
|
||||
|
const skipOverrideUpdates = options.skipWatcherUpdates || options.fromDataSource; |
||||
|
if (!skipOverrideUpdates) { |
||||
|
this.updateSymbolOverride(); |
||||
|
} |
||||
|
|
||||
|
return evaluatedProps; |
||||
|
} |
||||
|
|
||||
|
removeAttributes(attributes: string[]) { |
||||
|
this.attributeWatcher.removeListeners(attributes); |
||||
|
this.updateSymbolOverride(); |
||||
|
} |
||||
|
|
||||
|
private updateSymbolOverride() { |
||||
|
if (!this.component || !this.component.get(keyIsCollectionItem)) return; |
||||
|
|
||||
|
const keys = this.propertyWatcher.getDynamicValuesOfType(DataCollectionVariableType); |
||||
|
const attributesKeys = this.attributeWatcher.getDynamicValuesOfType(DataCollectionVariableType); |
||||
|
|
||||
|
const combinedKeys = [keyCollectionsStateMap, ...keys]; |
||||
|
const haveOverridenAttributes = Object.keys(attributesKeys).length; |
||||
|
if (haveOverridenAttributes) combinedKeys.push('attributes'); |
||||
|
|
||||
|
const toUp = getSymbolsToUpdate(this.component); |
||||
|
toUp.forEach((child) => { |
||||
|
child.setSymbolOverride(combinedKeys, { fromDataSource: true }); |
||||
|
}); |
||||
|
this.component.setSymbolOverride(combinedKeys, { fromDataSource: true }); |
||||
|
} |
||||
|
|
||||
|
getDynamicPropsDefs() { |
||||
|
return this.propertyWatcher.getAllSerializableValues(); |
||||
|
} |
||||
|
|
||||
|
getDynamicAttributesDefs() { |
||||
|
return this.attributeWatcher.getAllSerializableValues(); |
||||
|
} |
||||
|
|
||||
|
getPropsDefsOrValues(props: ObjectAny) { |
||||
|
return this.propertyWatcher.getSerializableValues(props); |
||||
|
} |
||||
|
|
||||
|
getAttributesDefsOrValues(attributes: ObjectAny) { |
||||
|
return this.attributeWatcher.getSerializableValues(attributes); |
||||
|
} |
||||
|
|
||||
|
destroy() { |
||||
|
this.propertyWatcher.destroy(); |
||||
|
this.attributeWatcher.destroy(); |
||||
|
} |
||||
|
} |
||||
@ -1,66 +0,0 @@ |
|||||
import { ObjectAny } from '../../common'; |
|
||||
import EditorModel from '../../editor/model/Editor'; |
|
||||
import Component from './Component'; |
|
||||
import { DynamicValueWatcher } from './DynamicValueWatcher'; |
|
||||
|
|
||||
export class ComponentDynamicValueWatcher { |
|
||||
private propertyWatcher: DynamicValueWatcher; |
|
||||
private attributeWatcher: DynamicValueWatcher; |
|
||||
|
|
||||
constructor( |
|
||||
private component: Component, |
|
||||
em: EditorModel, |
|
||||
) { |
|
||||
this.propertyWatcher = new DynamicValueWatcher(this.createPropertyUpdater(), em); |
|
||||
this.attributeWatcher = new DynamicValueWatcher(this.createAttributeUpdater(), em); |
|
||||
} |
|
||||
|
|
||||
private createPropertyUpdater() { |
|
||||
return (key: string, value: any) => { |
|
||||
this.component.set(key, value, { fromDataSource: true, avoidStore: true }); |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
private createAttributeUpdater() { |
|
||||
return (key: string, value: any) => { |
|
||||
this.component.addAttributes({ [key]: value }, { fromDataSource: true, avoidStore: true }); |
|
||||
}; |
|
||||
} |
|
||||
|
|
||||
addProps(props: ObjectAny) { |
|
||||
this.propertyWatcher.addDynamicValues(props); |
|
||||
} |
|
||||
|
|
||||
addAttributes(attributes: ObjectAny) { |
|
||||
this.attributeWatcher.addDynamicValues(attributes); |
|
||||
} |
|
||||
|
|
||||
setAttributes(attributes: ObjectAny) { |
|
||||
this.attributeWatcher.setDynamicValues(attributes); |
|
||||
} |
|
||||
|
|
||||
removeAttributes(attributes: string[]) { |
|
||||
this.attributeWatcher.removeListeners(attributes); |
|
||||
} |
|
||||
|
|
||||
getDynamicPropsDefs() { |
|
||||
return this.propertyWatcher.getAllSerializableValues(); |
|
||||
} |
|
||||
|
|
||||
getDynamicAttributesDefs() { |
|
||||
return this.attributeWatcher.getAllSerializableValues(); |
|
||||
} |
|
||||
|
|
||||
getAttributesDefsOrValues(attributes: ObjectAny) { |
|
||||
return this.attributeWatcher.getSerializableValues(attributes); |
|
||||
} |
|
||||
|
|
||||
getPropsDefsOrValues(props: ObjectAny) { |
|
||||
return this.propertyWatcher.getSerializableValues(props); |
|
||||
} |
|
||||
|
|
||||
destroy() { |
|
||||
this.propertyWatcher.removeListeners(); |
|
||||
this.attributeWatcher.removeListeners(); |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,180 @@ |
|||||
|
import { ObjectAny } from '../../common'; |
||||
|
import { DataCollectionVariableType } from '../../data_sources/model/data_collection/constants'; |
||||
|
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; |
||||
|
import DataResolverListener from '../../data_sources/model/DataResolverListener'; |
||||
|
import { getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/model/utils'; |
||||
|
import EditorModel from '../../editor/model/Editor'; |
||||
|
import { DataResolverProps } from '../../data_sources/types'; |
||||
|
import Component from './Component'; |
||||
|
|
||||
|
export interface DynamicWatchersOptions { |
||||
|
skipWatcherUpdates?: boolean; |
||||
|
fromDataSource?: boolean; |
||||
|
} |
||||
|
|
||||
|
export interface ComponentResolverWatcherOptions { |
||||
|
em: EditorModel; |
||||
|
collectionsStateMap?: DataCollectionStateMap; |
||||
|
} |
||||
|
|
||||
|
type UpdateFn = (component: Component | undefined, key: string, value: any) => void; |
||||
|
|
||||
|
export class ComponentResolverWatcher { |
||||
|
private em: EditorModel; |
||||
|
private collectionsStateMap?: DataCollectionStateMap; |
||||
|
private resolverListeners: Record<string, DataResolverListener> = {}; |
||||
|
|
||||
|
constructor( |
||||
|
private component: Component | undefined, |
||||
|
private updateFn: UpdateFn, |
||||
|
options: ComponentResolverWatcherOptions, |
||||
|
) { |
||||
|
this.em = options.em; |
||||
|
this.collectionsStateMap = options.collectionsStateMap; |
||||
|
} |
||||
|
|
||||
|
bindComponent(component: Component) { |
||||
|
this.component = component; |
||||
|
} |
||||
|
|
||||
|
updateCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { |
||||
|
this.collectionsStateMap = collectionsStateMap; |
||||
|
|
||||
|
const collectionVariablesKeys = this.getDynamicValuesOfType(DataCollectionVariableType); |
||||
|
const collectionVariablesObject = collectionVariablesKeys.reduce( |
||||
|
(acc: { [key: string]: DataResolverProps | null }, key) => { |
||||
|
acc[key] = null; |
||||
|
return acc; |
||||
|
}, |
||||
|
{}, |
||||
|
); |
||||
|
const newVariables = this.getSerializableValues(collectionVariablesObject); |
||||
|
const evaluatedValues = this.addDynamicValues(newVariables); |
||||
|
|
||||
|
Object.keys(evaluatedValues).forEach((key) => { |
||||
|
this.updateFn(this.component, key, evaluatedValues[key]); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
setDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) { |
||||
|
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource; |
||||
|
if (!shouldSkipWatcherUpdates) { |
||||
|
this.removeListeners(); |
||||
|
} |
||||
|
|
||||
|
return this.addDynamicValues(values, options); |
||||
|
} |
||||
|
|
||||
|
addDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) { |
||||
|
if (!values) return {}; |
||||
|
const evaluatedValues = this.evaluateValues(values); |
||||
|
|
||||
|
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource; |
||||
|
if (!shouldSkipWatcherUpdates) { |
||||
|
this.updateListeners(values); |
||||
|
} |
||||
|
|
||||
|
return evaluatedValues; |
||||
|
} |
||||
|
|
||||
|
private updateListeners(values: { [key: string]: any }) { |
||||
|
const { em, collectionsStateMap } = this; |
||||
|
this.removeListeners(Object.keys(values)); |
||||
|
const propsKeys = Object.keys(values); |
||||
|
|
||||
|
for (let index = 0; index < propsKeys.length; index++) { |
||||
|
const key = propsKeys[index]; |
||||
|
const resolverProps = values[key]; |
||||
|
|
||||
|
if (!isDataResolverProps(resolverProps)) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
const { resolver } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap }); |
||||
|
this.resolverListeners[key] = new DataResolverListener({ |
||||
|
em, |
||||
|
resolver, |
||||
|
onUpdate: (value) => this.updateFn.bind(this)(this.component, key, value), |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private evaluateValues(values: ObjectAny) { |
||||
|
const { em, collectionsStateMap } = this; |
||||
|
const evaluatedValues = { ...values }; |
||||
|
const propsKeys = Object.keys(values); |
||||
|
|
||||
|
for (let index = 0; index < propsKeys.length; index++) { |
||||
|
const key = propsKeys[index]; |
||||
|
const resolverProps = values[key]; |
||||
|
|
||||
|
if (!isDataResolverProps(resolverProps)) { |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
const { value } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap }); |
||||
|
evaluatedValues[key] = value; |
||||
|
} |
||||
|
|
||||
|
return evaluatedValues; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* removes listeners to stop watching for changes, |
||||
|
* if keys argument is omitted, remove all listeners |
||||
|
* @argument keys |
||||
|
*/ |
||||
|
removeListeners(keys?: string[]) { |
||||
|
const propsKeys = keys ? keys : Object.keys(this.resolverListeners); |
||||
|
|
||||
|
propsKeys.forEach((key) => { |
||||
|
if (this.resolverListeners[key]) { |
||||
|
this.resolverListeners[key].destroy?.(); |
||||
|
delete this.resolverListeners[key]; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return propsKeys; |
||||
|
} |
||||
|
|
||||
|
getSerializableValues(values: ObjectAny | undefined) { |
||||
|
if (!values) return {}; |
||||
|
const serializableValues = { ...values }; |
||||
|
const propsKeys = Object.keys(serializableValues); |
||||
|
|
||||
|
for (let index = 0; index < propsKeys.length; index++) { |
||||
|
const key = propsKeys[index]; |
||||
|
const resolverListener = this.resolverListeners[key]; |
||||
|
if (resolverListener) { |
||||
|
serializableValues[key] = resolverListener.resolver.toJSON(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return serializableValues; |
||||
|
} |
||||
|
|
||||
|
getAllSerializableValues() { |
||||
|
const serializableValues: ObjectAny = {}; |
||||
|
const propsKeys = Object.keys(this.resolverListeners); |
||||
|
|
||||
|
for (let index = 0; index < propsKeys.length; index++) { |
||||
|
const key = propsKeys[index]; |
||||
|
serializableValues[key] = this.resolverListeners[key].resolver.toJSON(); |
||||
|
} |
||||
|
|
||||
|
return serializableValues; |
||||
|
} |
||||
|
|
||||
|
getDynamicValuesOfType(type: DataResolverProps['type']) { |
||||
|
const keys = Object.keys(this.resolverListeners).filter((key: string) => { |
||||
|
// @ts-ignore
|
||||
|
return this.resolverListeners[key].resolver.get('type') === type; |
||||
|
}); |
||||
|
|
||||
|
return keys; |
||||
|
} |
||||
|
|
||||
|
destroy() { |
||||
|
this.removeListeners(); |
||||
|
} |
||||
|
} |
||||
@ -1,117 +0,0 @@ |
|||||
import { ObjectAny } from '../../common'; |
|
||||
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; |
|
||||
import { evaluateDynamicValueDefinition, isDynamicValueDefinition } from '../../data_sources/model/utils'; |
|
||||
import { DynamicValue } from '../../data_sources/types'; |
|
||||
import EditorModel from '../../editor/model/Editor'; |
|
||||
|
|
||||
export class DynamicValueWatcher { |
|
||||
dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {}; |
|
||||
constructor( |
|
||||
private updateFn: (key: string, value: any) => void, |
|
||||
private em: EditorModel, |
|
||||
) {} |
|
||||
|
|
||||
static getStaticValues(values: ObjectAny | undefined, em: EditorModel): ObjectAny { |
|
||||
if (!values) return {}; |
|
||||
const evaluatedValues: ObjectAny = { ...values }; |
|
||||
const propsKeys = Object.keys(values); |
|
||||
|
|
||||
for (const key of propsKeys) { |
|
||||
const valueDefinition = values[key]; |
|
||||
if (!isDynamicValueDefinition(valueDefinition)) continue; |
|
||||
|
|
||||
const { value } = evaluateDynamicValueDefinition(valueDefinition, em); |
|
||||
evaluatedValues[key] = value; |
|
||||
} |
|
||||
|
|
||||
return evaluatedValues; |
|
||||
} |
|
||||
|
|
||||
static areStaticValues(values: ObjectAny | undefined) { |
|
||||
if (!values) return true; |
|
||||
return Object.keys(values).every((key) => { |
|
||||
return !isDynamicValueDefinition(values[key]); |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
setDynamicValues(values: ObjectAny | undefined) { |
|
||||
this.removeListeners(); |
|
||||
return this.addDynamicValues(values); |
|
||||
} |
|
||||
|
|
||||
addDynamicValues(values: ObjectAny | undefined) { |
|
||||
if (!values) return {}; |
|
||||
this.removeListeners(Object.keys(values)); |
|
||||
const dynamicProps = this.getDynamicValues(values); |
|
||||
const propsKeys = Object.keys(dynamicProps); |
|
||||
for (let index = 0; index < propsKeys.length; index++) { |
|
||||
const key = propsKeys[index]; |
|
||||
this.dynamicVariableListeners[key] = new DynamicVariableListenerManager({ |
|
||||
em: this.em, |
|
||||
dataVariable: dynamicProps[key], |
|
||||
updateValueFromDataVariable: (value: any) => { |
|
||||
this.updateFn.bind(this)(key, value); |
|
||||
}, |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
return dynamicProps; |
|
||||
} |
|
||||
|
|
||||
private getDynamicValues(values: ObjectAny) { |
|
||||
const dynamicValues: { |
|
||||
[key: string]: DynamicValue; |
|
||||
} = {}; |
|
||||
const propsKeys = Object.keys(values); |
|
||||
for (let index = 0; index < propsKeys.length; index++) { |
|
||||
const key = propsKeys[index]; |
|
||||
if (!isDynamicValueDefinition(values[key])) { |
|
||||
continue; |
|
||||
} |
|
||||
const { variable } = evaluateDynamicValueDefinition(values[key], this.em); |
|
||||
dynamicValues[key] = variable; |
|
||||
} |
|
||||
|
|
||||
return dynamicValues; |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* removes listeners to stop watching for changes, |
|
||||
* if keys argument is omitted, remove all listeners |
|
||||
* @argument keys |
|
||||
*/ |
|
||||
removeListeners(keys?: string[]) { |
|
||||
const propsKeys = keys ? keys : Object.keys(this.dynamicVariableListeners); |
|
||||
propsKeys.forEach((key) => { |
|
||||
if (this.dynamicVariableListeners[key]) { |
|
||||
this.dynamicVariableListeners[key].destroy(); |
|
||||
delete this.dynamicVariableListeners[key]; |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
getSerializableValues(values: ObjectAny | undefined) { |
|
||||
if (!values) return {}; |
|
||||
const serializableValues = { ...values }; |
|
||||
const propsKeys = Object.keys(serializableValues); |
|
||||
for (let index = 0; index < propsKeys.length; index++) { |
|
||||
const key = propsKeys[index]; |
|
||||
if (this.dynamicVariableListeners[key]) { |
|
||||
serializableValues[key] = this.dynamicVariableListeners[key].dynamicVariable.toJSON(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return serializableValues; |
|
||||
} |
|
||||
|
|
||||
getAllSerializableValues() { |
|
||||
const serializableValues: ObjectAny = {}; |
|
||||
const propsKeys = Object.keys(this.dynamicVariableListeners); |
|
||||
for (let index = 0; index < propsKeys.length; index++) { |
|
||||
const key = propsKeys[index]; |
|
||||
serializableValues[key] = this.dynamicVariableListeners[key].dynamicVariable.toJSON(); |
|
||||
} |
|
||||
|
|
||||
return serializableValues; |
|
||||
} |
|
||||
} |
|
||||
File diff suppressed because it is too large
@ -0,0 +1,264 @@ |
|||||
|
import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src'; |
||||
|
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; |
||||
|
import { |
||||
|
DataCollectionType, |
||||
|
DataCollectionVariableType, |
||||
|
} from '../../../../../src/data_sources/model/data_collection/constants'; |
||||
|
import { DataCollectionStateVariableType } from '../../../../../src/data_sources/model/data_collection/types'; |
||||
|
import EditorModel from '../../../../../src/editor/model/Editor'; |
||||
|
import { ProjectData } from '../../../../../src/storage_manager'; |
||||
|
import { setupTestEditor } from '../../../../common'; |
||||
|
|
||||
|
describe('Collection variable components', () => { |
||||
|
let em: EditorModel; |
||||
|
let editor: Editor; |
||||
|
let dsm: DataSourceManager; |
||||
|
let dataSource: DataSource; |
||||
|
let wrapper: Component; |
||||
|
let firstRecord: DataRecord; |
||||
|
let secondRecord: DataRecord; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
({ em, editor, dsm } = setupTestEditor()); |
||||
|
wrapper = em.getWrapper()!; |
||||
|
dataSource = dsm.add({ |
||||
|
id: 'my_data_source_id', |
||||
|
records: [ |
||||
|
{ id: 'user1', user: 'user1', age: '12' }, |
||||
|
{ id: 'user2', user: 'user2', age: '14' }, |
||||
|
{ id: 'user3', user: 'user3', age: '16' }, |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
firstRecord = dataSource.getRecord('user1')!; |
||||
|
secondRecord = dataSource.getRecord('user2')!; |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
em.destroy(); |
||||
|
}); |
||||
|
|
||||
|
test('Gets the correct static value', async () => { |
||||
|
const cmp = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
components: [ |
||||
|
{ |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'my_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0]; |
||||
|
|
||||
|
const firstGrandchild = cmp.components().at(0).components().at(0); |
||||
|
expect(firstGrandchild.getInnerHTML()).toContain('user1'); |
||||
|
expect(firstGrandchild.getEl()?.innerHTML).toContain('user1'); |
||||
|
|
||||
|
const secondGrandchild = cmp.components().at(1).components().at(0); |
||||
|
expect(secondGrandchild.getInnerHTML()).toContain('user2'); |
||||
|
expect(secondGrandchild.getEl()?.innerHTML).toContain('user2'); |
||||
|
}); |
||||
|
|
||||
|
test('Watches collection variable changes', async () => { |
||||
|
const cmp = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
components: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'my_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0]; |
||||
|
firstRecord.set('user', 'new_correct_value'); |
||||
|
|
||||
|
const firstGrandchild = cmp.components().at(0).components().at(0); |
||||
|
expect(firstGrandchild.getInnerHTML()).toContain('new_correct_value'); |
||||
|
expect(firstGrandchild.getEl()?.innerHTML).toContain('new_correct_value'); |
||||
|
|
||||
|
const secondGrandchild = cmp.components().at(1).components().at(0); |
||||
|
expect(secondGrandchild.getInnerHTML()).toContain('user2'); |
||||
|
expect(secondGrandchild.getEl()?.innerHTML).toContain('user2'); |
||||
|
}); |
||||
|
|
||||
|
describe('Serialization', () => { |
||||
|
let cmp: Component; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
const variableCmpDef = { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}; |
||||
|
|
||||
|
const collectionComponentDefinition = { |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
components: [ |
||||
|
{ |
||||
|
type: 'default', |
||||
|
}, |
||||
|
variableCmpDef, |
||||
|
], |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'my_collection', |
||||
|
startIndex: 0, |
||||
|
endIndex: 2, |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
cmp = wrapper.components(collectionComponentDefinition)[0]; |
||||
|
}); |
||||
|
|
||||
|
test('Serializion to JSON', () => { |
||||
|
expect(cmp.toJSON()).toMatchSnapshot(`Collection with collection variable component ( no grandchildren )`); |
||||
|
|
||||
|
const firstChild = cmp.components().at(0); |
||||
|
const newChildDefinition = { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentIndex, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}; |
||||
|
firstChild.components().at(0).components(newChildDefinition); |
||||
|
expect(cmp.toJSON()).toMatchSnapshot(`Collection with collection variable component ( with grandchildren )`); |
||||
|
}); |
||||
|
|
||||
|
test('Saving', () => { |
||||
|
const projectData = editor.getProjectData(); |
||||
|
const page = projectData.pages[0]; |
||||
|
const frame = page.frames[0]; |
||||
|
const component = frame.component.components[0]; |
||||
|
|
||||
|
expect(component).toMatchSnapshot(`Collection with collection variable component ( no grandchildren )`); |
||||
|
|
||||
|
const firstChild = cmp.components().at(0); |
||||
|
const newChildDefinition = { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentIndex, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}; |
||||
|
|
||||
|
firstChild.components().at(0).components(newChildDefinition); |
||||
|
expect(cmp.toJSON()).toMatchSnapshot(`Collection with collection variable component ( with grandchildren )`); |
||||
|
}); |
||||
|
|
||||
|
test('Loading', () => { |
||||
|
const componentProjectData: ProjectData = { |
||||
|
assets: [], |
||||
|
pages: [ |
||||
|
{ |
||||
|
frames: [ |
||||
|
{ |
||||
|
component: { |
||||
|
components: [ |
||||
|
{ |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
components: [ |
||||
|
{ |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'my_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'my_collection', |
||||
|
dataSource: { |
||||
|
path: 'my_data_source_id', |
||||
|
type: DataVariableType, |
||||
|
}, |
||||
|
endIndex: 1, |
||||
|
startIndex: 0, |
||||
|
}, |
||||
|
}, |
||||
|
type: DataCollectionType, |
||||
|
}, |
||||
|
], |
||||
|
docEl: { |
||||
|
tagName: 'html', |
||||
|
}, |
||||
|
head: { |
||||
|
type: 'head', |
||||
|
}, |
||||
|
stylable: [ |
||||
|
'background', |
||||
|
'background-color', |
||||
|
'background-image', |
||||
|
'background-repeat', |
||||
|
'background-attachment', |
||||
|
'background-position', |
||||
|
'background-size', |
||||
|
], |
||||
|
type: 'wrapper', |
||||
|
}, |
||||
|
id: 'frameid', |
||||
|
}, |
||||
|
], |
||||
|
id: 'pageid', |
||||
|
type: 'main', |
||||
|
}, |
||||
|
], |
||||
|
styles: [], |
||||
|
symbols: [], |
||||
|
dataSources: [dataSource], |
||||
|
}; |
||||
|
editor.loadProjectData(componentProjectData); |
||||
|
|
||||
|
const components = editor.getComponents(); |
||||
|
const component = components.models[0]; |
||||
|
const firstChild = component.components().at(0); |
||||
|
const firstGrandchild = firstChild.components().at(0); |
||||
|
const secondChild = component.components().at(1); |
||||
|
const secondGrandchild = secondChild.components().at(0); |
||||
|
|
||||
|
expect(firstGrandchild.getInnerHTML()).toBe('user1'); |
||||
|
expect(secondGrandchild.getInnerHTML()).toBe('user2'); |
||||
|
|
||||
|
firstRecord.set('user', 'new_user1_value'); |
||||
|
expect(firstGrandchild.getInnerHTML()).toBe('new_user1_value'); |
||||
|
expect(secondGrandchild.getInnerHTML()).toBe('user2'); |
||||
|
|
||||
|
secondRecord.set('user', 'new_user2_value'); |
||||
|
expect(firstGrandchild.getInnerHTML()).toBe('new_user1_value'); |
||||
|
expect(secondGrandchild.getInnerHTML()).toBe('new_user2_value'); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
@ -0,0 +1,519 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`Collection component Serialization Saving: Collection with grandchildren 1`] = ` |
||||
|
{ |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "my_collection", |
||||
|
"dataSource": { |
||||
|
"path": "my_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
"endIndex": 1, |
||||
|
"startIndex": 0, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"components": [ |
||||
|
{ |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"components": [ |
||||
|
{ |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
], |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
{ |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
], |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
} |
||||
|
`; |
||||
|
|
||||
|
exports[`Collection component Serialization Saving: Collection with no grandchildren 1`] = ` |
||||
|
{ |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "my_collection", |
||||
|
"dataSource": { |
||||
|
"path": "my_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
"endIndex": 1, |
||||
|
"startIndex": 0, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"components": [ |
||||
|
{ |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
{ |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
], |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
} |
||||
|
`; |
||||
|
|
||||
|
exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with grandchildren 1`] = ` |
||||
|
{ |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "my_collection", |
||||
|
"dataSource": { |
||||
|
"path": "my_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
"endIndex": 1, |
||||
|
"startIndex": 0, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"components": [ |
||||
|
{ |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"components": [ |
||||
|
{ |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
], |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
{ |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
], |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
} |
||||
|
`; |
||||
|
|
||||
|
exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with no grandchildren 1`] = ` |
||||
|
{ |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "my_collection", |
||||
|
"dataSource": { |
||||
|
"path": "my_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
"endIndex": 1, |
||||
|
"startIndex": 0, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"components": [ |
||||
|
{ |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
{ |
||||
|
"attributes": { |
||||
|
"attribute_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
}, |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
], |
||||
|
"custom_prop": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
"name": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"property_trait": { |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
} |
||||
|
`; |
||||
@ -0,0 +1,141 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`Collection variable components Serialization Saving: Collection with collection variable component ( no grandchildren ) 1`] = ` |
||||
|
{ |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "my_collection", |
||||
|
"dataSource": { |
||||
|
"path": "my_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
"endIndex": 2, |
||||
|
"startIndex": 0, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"components": [ |
||||
|
{ |
||||
|
"type": "default", |
||||
|
}, |
||||
|
{ |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
], |
||||
|
"type": "default", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
} |
||||
|
`; |
||||
|
|
||||
|
exports[`Collection variable components Serialization Saving: Collection with collection variable component ( with grandchildren ) 1`] = ` |
||||
|
{ |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "my_collection", |
||||
|
"dataSource": { |
||||
|
"path": "my_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
"endIndex": 2, |
||||
|
"startIndex": 0, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"components": [ |
||||
|
{ |
||||
|
"components": [ |
||||
|
{ |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
], |
||||
|
"type": "default", |
||||
|
}, |
||||
|
{ |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
], |
||||
|
"type": "default", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
} |
||||
|
`; |
||||
|
|
||||
|
exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( no grandchildren ) 1`] = ` |
||||
|
{ |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "my_collection", |
||||
|
"dataSource": { |
||||
|
"path": "my_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
"endIndex": 2, |
||||
|
"startIndex": 0, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"components": [ |
||||
|
{ |
||||
|
"type": "default", |
||||
|
}, |
||||
|
{ |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
], |
||||
|
"type": "default", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
} |
||||
|
`; |
||||
|
|
||||
|
exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( with grandchildren ) 1`] = ` |
||||
|
{ |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "my_collection", |
||||
|
"dataSource": { |
||||
|
"path": "my_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
"endIndex": 2, |
||||
|
"startIndex": 0, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"components": [ |
||||
|
{ |
||||
|
"components": [ |
||||
|
{ |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentIndex", |
||||
|
}, |
||||
|
], |
||||
|
"type": "default", |
||||
|
}, |
||||
|
{ |
||||
|
"collectionId": "my_collection", |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
], |
||||
|
"type": "default", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
} |
||||
|
`; |
||||
@ -0,0 +1,36 @@ |
|||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP |
||||
|
|
||||
|
exports[`Collection component Nested collections are correctly serialized 1`] = ` |
||||
|
{ |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "parent_collection", |
||||
|
"dataSource": { |
||||
|
"path": "my_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"collectionDef": { |
||||
|
"collectionConfig": { |
||||
|
"collectionId": "nested_collection", |
||||
|
"dataSource": { |
||||
|
"path": "nested_data_source_id", |
||||
|
"type": "data-variable", |
||||
|
}, |
||||
|
}, |
||||
|
"componentDef": { |
||||
|
"name": { |
||||
|
"path": "user", |
||||
|
"type": "data-collection-variable", |
||||
|
"variableType": "currentItem", |
||||
|
}, |
||||
|
"type": "default", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
}, |
||||
|
}, |
||||
|
"type": "data-collection", |
||||
|
} |
||||
|
`; |
||||
@ -0,0 +1,430 @@ |
|||||
|
import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src'; |
||||
|
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; |
||||
|
import { |
||||
|
DataCollectionType, |
||||
|
DataCollectionVariableType, |
||||
|
} from '../../../../../src/data_sources/model/data_collection/constants'; |
||||
|
import { DataCollectionStateVariableType } from '../../../../../src/data_sources/model/data_collection/types'; |
||||
|
import EditorModel from '../../../../../src/editor/model/Editor'; |
||||
|
import { setupTestEditor } from '../../../../common'; |
||||
|
|
||||
|
describe('Collection component', () => { |
||||
|
let em: EditorModel; |
||||
|
let editor: Editor; |
||||
|
let dsm: DataSourceManager; |
||||
|
let dataSource: DataSource; |
||||
|
let nestedDataSource: DataSource; |
||||
|
let wrapper: Component; |
||||
|
let firstRecord: DataRecord; |
||||
|
let secondRecord: DataRecord; |
||||
|
let firstNestedRecord: DataRecord; |
||||
|
let secondNestedRecord: DataRecord; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
({ em, editor, dsm } = setupTestEditor()); |
||||
|
wrapper = em.getWrapper()!; |
||||
|
dataSource = dsm.add({ |
||||
|
id: 'my_data_source_id', |
||||
|
records: [ |
||||
|
{ id: 'user1', user: 'user1', age: '12' }, |
||||
|
{ id: 'user2', user: 'user2', age: '14' }, |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
nestedDataSource = dsm.add({ |
||||
|
id: 'nested_data_source_id', |
||||
|
records: [ |
||||
|
{ id: 'nested_user1', user: 'nested_user1', age: '12' }, |
||||
|
{ id: 'nested_user2', user: 'nested_user2', age: '14' }, |
||||
|
{ id: 'nested_user3', user: 'nested_user3', age: '16' }, |
||||
|
], |
||||
|
}); |
||||
|
|
||||
|
firstRecord = dataSource.getRecord('user1')!; |
||||
|
secondRecord = dataSource.getRecord('user2')!; |
||||
|
firstNestedRecord = nestedDataSource.getRecord('nested_user1')!; |
||||
|
secondNestedRecord = nestedDataSource.getRecord('nested_user2')!; |
||||
|
}); |
||||
|
|
||||
|
afterEach(() => { |
||||
|
em.destroy(); |
||||
|
}); |
||||
|
|
||||
|
test('Nested collections bind to correct data sources', () => { |
||||
|
const parentCollection = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
name: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'nested_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'nested_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'nested_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'parent_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0]; |
||||
|
|
||||
|
const nestedCollection = parentCollection.components().at(0); |
||||
|
const nestedFirstChild = nestedCollection.components().at(0); |
||||
|
const nestedSecondChild = nestedCollection.components().at(1); |
||||
|
|
||||
|
expect(nestedFirstChild.get('name')).toBe('nested_user1'); |
||||
|
expect(nestedSecondChild.get('name')).toBe('nested_user2'); |
||||
|
}); |
||||
|
|
||||
|
test('Updates in parent collection propagate to nested collections', () => { |
||||
|
const parentCollection = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
name: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'nested_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'nested_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'nested_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'parent_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0]; |
||||
|
|
||||
|
const nestedCollection = parentCollection.components().at(0); |
||||
|
const nestedFirstChild = nestedCollection.components().at(0); |
||||
|
const nestedSecondChild = nestedCollection.components().at(1); |
||||
|
|
||||
|
firstNestedRecord.set('user', 'updated_user1'); |
||||
|
expect(nestedFirstChild.get('name')).toBe('updated_user1'); |
||||
|
expect(nestedSecondChild.get('name')).toBe('nested_user2'); |
||||
|
}); |
||||
|
|
||||
|
test('Nested collections are correctly serialized', () => { |
||||
|
const parentCollection = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
name: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'nested_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'nested_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'parent_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0]; |
||||
|
|
||||
|
const serialized = parentCollection.toJSON(); |
||||
|
expect(serialized).toMatchSnapshot(); |
||||
|
}); |
||||
|
|
||||
|
test('Nested collections respect startIndex and endIndex', () => { |
||||
|
const parentCollection = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
name: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'nested_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'nested_collection', |
||||
|
startIndex: 0, |
||||
|
endIndex: 1, |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'nested_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'parent_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0]; |
||||
|
|
||||
|
const nestedCollection = parentCollection.components().at(0); |
||||
|
expect(nestedCollection.components().length).toBe(2); |
||||
|
}); |
||||
|
|
||||
|
test('Nested collection gets and watches value from the parent collection', () => { |
||||
|
const parentCollection = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
name: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'parent_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'nested_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'nested_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'parent_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0]; |
||||
|
|
||||
|
const nestedCollection = parentCollection.components().at(0); |
||||
|
const firstNestedChild = nestedCollection.components().at(0); |
||||
|
|
||||
|
// Verify initial value
|
||||
|
expect(firstNestedChild.get('name')).toBe('user1'); |
||||
|
|
||||
|
// Update value in parent collection and verify nested collection updates
|
||||
|
firstRecord.set('user', 'updated_user1'); |
||||
|
expect(firstNestedChild.get('name')).toBe('updated_user1'); |
||||
|
}); |
||||
|
|
||||
|
test('Nested collection switches to using its own collection variable', () => { |
||||
|
const parentCollection = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
name: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
path: 'user', |
||||
|
collectionId: 'parent_collection', |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'nested_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'nested_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'parent_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0]; |
||||
|
|
||||
|
const nestedCollection = parentCollection.components().at(0); |
||||
|
|
||||
|
const firstChild = nestedCollection.components().at(0); |
||||
|
// Replace the collection variable with one from the inner collection
|
||||
|
firstChild.set('name', { |
||||
|
// @ts-ignore
|
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
path: 'user', |
||||
|
collectionId: 'nested_collection', |
||||
|
}); |
||||
|
|
||||
|
expect(firstChild.get('name')).toBe('nested_user1'); |
||||
|
}); |
||||
|
|
||||
|
describe('Nested Collection Component with Parent and Nested Data Sources', () => { |
||||
|
let parentCollection: Component; |
||||
|
let nestedCollection: Component; |
||||
|
|
||||
|
beforeEach(() => { |
||||
|
// Initialize the parent and nested collections
|
||||
|
parentCollection = wrapper.components({ |
||||
|
type: DataCollectionType, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: DataCollectionType, |
||||
|
name: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'parent_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
collectionDef: { |
||||
|
componentDef: { |
||||
|
type: 'default', |
||||
|
name: { |
||||
|
type: DataCollectionVariableType, |
||||
|
variableType: DataCollectionStateVariableType.currentItem, |
||||
|
collectionId: 'nested_collection', |
||||
|
path: 'user', |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'nested_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'nested_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
collectionConfig: { |
||||
|
collectionId: 'parent_collection', |
||||
|
dataSource: { |
||||
|
type: DataVariableType, |
||||
|
path: 'my_data_source_id', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
})[0]; |
||||
|
|
||||
|
nestedCollection = parentCollection.components().at(0); |
||||
|
}); |
||||
|
|
||||
|
test('Removing a record from the parent data source updates the parent collection correctly', () => { |
||||
|
// Verify initial state
|
||||
|
expect(parentCollection.components().length).toBe(2); // 2 parent records initially
|
||||
|
|
||||
|
// Remove a record from the parent data source
|
||||
|
dataSource.removeRecord('user1'); |
||||
|
|
||||
|
// Verify that the parent collection updates correctly
|
||||
|
expect(parentCollection.components().length).toBe(1); // Only 1 parent record remains
|
||||
|
expect(parentCollection.components().at(0).get('name')).toBe('user2'); // Verify updated name
|
||||
|
|
||||
|
// Verify that the nested collection is unaffected
|
||||
|
expect(nestedCollection.components().length).toBe(3); // Nested records remain the same
|
||||
|
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify nested name
|
||||
|
}); |
||||
|
|
||||
|
test('Adding a record to the parent data source updates the parent collection correctly', () => { |
||||
|
// Verify initial state
|
||||
|
expect(parentCollection.components().length).toBe(2); // 2 parent records initially
|
||||
|
|
||||
|
// Add a new record to the parent data source
|
||||
|
dataSource.addRecord({ id: 'user3', user: 'user3', age: '16' }); |
||||
|
|
||||
|
// Verify that the parent collection updates correctly
|
||||
|
expect(parentCollection.components().length).toBe(3); // 3 parent records now
|
||||
|
expect(parentCollection.components().at(2).get('name')).toBe('user3'); // Verify new name
|
||||
|
|
||||
|
// Verify that the nested collection is unaffected
|
||||
|
expect(nestedCollection.components().length).toBe(3); // Nested records remain the same
|
||||
|
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify nested name
|
||||
|
expect(parentCollection.components().at(2).components().at(0).get('name')).toBe('nested_user1'); // Verify nested name
|
||||
|
}); |
||||
|
|
||||
|
test('Removing a record from the nested data source updates the nested collection correctly', () => { |
||||
|
// Verify initial state
|
||||
|
expect(nestedCollection.components().length).toBe(3); // 3 nested records initially
|
||||
|
|
||||
|
// Remove a record from the nested data source
|
||||
|
nestedDataSource.removeRecord('nested_user1'); |
||||
|
|
||||
|
// Verify that the nested collection updates correctly
|
||||
|
expect(nestedCollection.components().length).toBe(2); // Only 2 nested records remain
|
||||
|
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user2'); // Verify updated name
|
||||
|
expect(nestedCollection.components().at(1).get('name')).toBe('nested_user3'); // Verify updated name
|
||||
|
}); |
||||
|
|
||||
|
test('Adding a record to the nested data source updates the nested collection correctly', () => { |
||||
|
// Verify initial state
|
||||
|
expect(nestedCollection.components().length).toBe(3); // 3 nested records initially
|
||||
|
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify initial name
|
||||
|
expect(nestedCollection.components().at(1).get('name')).toBe('nested_user2'); // Verify initial name
|
||||
|
expect(nestedCollection.components().at(2).get('name')).toBe('nested_user3'); // Verify initial name
|
||||
|
|
||||
|
// Add a new record to the nested data source
|
||||
|
nestedDataSource.addRecord({ id: 'user4', user: 'nested_user4', age: '18' }); |
||||
|
|
||||
|
// Verify that the nested collection updates correctly
|
||||
|
expect(nestedCollection.components().length).toBe(4); // 4 nested records now
|
||||
|
expect(nestedCollection.components().at(3).get('name')).toBe('nested_user4'); // Verify new name
|
||||
|
|
||||
|
// Verify existing records are unaffected
|
||||
|
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify existing name
|
||||
|
expect(nestedCollection.components().at(1).get('name')).toBe('nested_user2'); // Verify existing name
|
||||
|
expect(nestedCollection.components().at(2).get('name')).toBe('nested_user3'); // Verify existing name
|
||||
|
}); |
||||
|
}); |
||||
|
}); |
||||
Loading…
Reference in new issue