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 { DynamicValue, DynamicValueDefinition } from '../types'; |
|||
import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition'; |
|||
import DataVariable, { DataVariableType } from './DataVariable'; |
|||
import { DataResolver, DataResolverProps } from '../types'; |
|||
import { DataCollectionStateMap } from './data_collection/types'; |
|||
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 { |
|||
return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value?.type); |
|||
export function isDataResolverProps(value: any): value is DataResolverProps { |
|||
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; |
|||
} |
|||
|
|||
export function isDataVariable(variable: any) { |
|||
export function isDataVariable(variable: any): variable is DataVariableProps { |
|||
return variable?.type === DataVariableType; |
|||
} |
|||
|
|||
export function isDataCondition(variable: any) { |
|||
return variable?.type === ConditionalVariableType; |
|||
return variable?.type === DataConditionType; |
|||
} |
|||
|
|||
export function evaluateVariable(variable: any, em: EditorModel) { |
|||
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; |
|||
} |
|||
|
|||
export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, em: EditorModel): DynamicValue { |
|||
const dynamicType = valueDefinition.type; |
|||
let dynamicVariable: DynamicValue; |
|||
export function getDataResolverInstance( |
|||
resolverProps: DataResolverProps, |
|||
options: { em: EditorModel; collectionsStateMap?: DataCollectionStateMap }, |
|||
): DataResolver { |
|||
const { type } = resolverProps; |
|||
let resolver: DataResolver; |
|||
|
|||
switch (dynamicType) { |
|||
switch (type) { |
|||
case DataVariableType: |
|||
dynamicVariable = new DataVariable(valueDefinition, { em: em }); |
|||
resolver = new DataVariable(resolverProps, options); |
|||
break; |
|||
case ConditionalVariableType: { |
|||
const { condition, ifTrue, ifFalse } = valueDefinition; |
|||
dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: em }); |
|||
case DataConditionType: { |
|||
const { condition, ifTrue, ifFalse } = resolverProps; |
|||
resolver = new DataCondition(condition, ifTrue, ifFalse, options); |
|||
break; |
|||
} |
|||
case DataCollectionVariableType: { |
|||
resolver = new DataCollectionVariable(resolverProps, options); |
|||
break; |
|||
} |
|||
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) { |
|||
const dynamicVariable = getDynamicValueInstance(valueDefinition, em); |
|||
export function getDataResolverInstanceValue( |
|||
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 ComponentDataVariable from '../model/ComponentDataVariable'; |
|||
import DynamicVariableListenerManager from '../model/DataVariableListenerManager'; |
|||
import DataResolverListener from '../model/DataResolverListener'; |
|||
|
|||
export default class ComponentDataVariableView extends ComponentView<ComponentDataVariable> { |
|||
dynamicVariableListener?: DynamicVariableListenerManager; |
|||
dataResolverListener!: DataResolverListener; |
|||
|
|||
initialize(opt = {}) { |
|||
super.initialize(opt); |
|||
this.dynamicVariableListener = new DynamicVariableListenerManager({ |
|||
em: this.em!, |
|||
dataVariable: this.model, |
|||
updateValueFromDataVariable: () => this.postRender(), |
|||
this.dataResolverListener = new DataResolverListener({ |
|||
em: this.em, |
|||
resolver: this.model.dataResolver, |
|||
onUpdate: () => this.postRender(), |
|||
}); |
|||
} |
|||
|
|||
remove() { |
|||
this.dataResolverListener.destroy(); |
|||
return super.remove(); |
|||
} |
|||
|
|||
postRender() { |
|||
const { model, el, em } = this; |
|||
const { path, defaultValue } = model.attributes; |
|||
el.innerHTML = em.DataSources.getValue(path, defaultValue); |
|||
this.el.innerHTML = this.model.getDataValue(); |
|||
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