From 98371167721804715b1774cfb8edb68aae2527cc Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 3 Jan 2025 10:51:51 +0200 Subject: [PATCH] Add collection variables --- .../CollectionComponent.ts | 114 ++++-------------- .../CollectionVariable.ts | 91 ++++++++++++++ .../model/collection_component/constants.ts | 4 + .../model/collection_component/types.ts | 34 ++++++ packages/core/src/data_sources/model/utils.ts | 33 ++++- packages/core/src/data_sources/types.ts | 9 +- .../src/dom_components/model/Component.ts | 30 ++++- .../model/ComponentDynamicValueWatcher.ts | 10 +- .../model/DynamicValueWatcher.ts | 30 ++++- .../dom_components/model/ComponentTypes.ts | 8 ++ 10 files changed, 252 insertions(+), 111 deletions(-) create mode 100644 packages/core/src/data_sources/model/collection_component/CollectionVariable.ts create mode 100644 packages/core/src/data_sources/model/collection_component/constants.ts create mode 100644 packages/core/src/data_sources/model/collection_component/types.ts diff --git a/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts b/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts index 4a3c70102..2be747d91 100644 --- a/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts +++ b/packages/core/src/data_sources/model/collection_component/CollectionComponent.ts @@ -1,60 +1,24 @@ -import { DataVariableDefinition, DataVariableType } from './../DataVariable'; +import { DataVariableType } from './../DataVariable'; import { isArray } from 'underscore'; import Component from '../../../dom_components/model/Component'; import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; -import { ConditionDefinition } from '../conditional_variables/DataCondition'; import DataSource from '../DataSource'; import { ObjectAny } from '../../../common'; import EditorModel from '../../../editor/model/Editor'; - -export const CollectionComponentType = 'collection-component'; -export const CollectionVariableType = 'parent-collection-variable'; - -type CollectionVariable = { - type: typeof CollectionVariableType; - variable_type: keyof CollectionState; - collection_name?: string; - path?: string; -}; - -type CollectionDataSource = any[] | DataVariableDefinition | CollectionVariable; - -type CollectionConfig = { - start_index?: number; - end_index?: number | ConditionDefinition; - dataSource: CollectionDataSource; -}; - -type CollectionState = { - current_index: number; - start_index: number; - current_item: any; - end_index: number; - collection_name?: string; - total_items: number; - remaining_items: number; -}; - -type CollectionsStateMap = { - [key: string]: CollectionState; -}; - -type CollectionDefinition = { - type: typeof CollectionComponentType; - collection_name?: string; - config: CollectionConfig; - block: ComponentDefinition; -}; - -export const collectionDefinitionKey = 'collectionDefinition'; -export const collectionsStateMapKey = 'collectionsItems'; -export const innerCollectionStateKey = 'innerCollectionState'; +import { keyCollectionsStateMap } from '../../../dom_components/model/Component'; +import { CollectionDefinition, CollectionState, CollectionsStateMap } from './types'; +import { + keyCollectionDefinition, + keyInnerCollectionState, + CollectionComponentType, + CollectionVariableType, +} from './constants'; export default class CollectionComponent extends Component { constructor(props: CollectionDefinition & ComponentProperties, opt: ComponentOptions) { const em = opt.em; - const { collection_name, block, config } = props[collectionDefinitionKey]; + const { collection_name, block, config } = props[keyCollectionDefinition]; if (!block) { throw new Error('The "block" property is required in the collection definition.'); } @@ -83,16 +47,22 @@ export default class CollectionComponent extends Component { }; const collectionsStateMap: CollectionsStateMap = { - ...props[collectionsStateMapKey], + ...props[keyCollectionsStateMap], ...(collection_name && { [collection_name]: collectionState }), - [innerCollectionStateKey]: collectionState, + [keyInnerCollectionState]: collectionState, }; if (index === start_index) { const { clonedBlock } = resolveBlockValues(collectionsStateMap, block); const type = em.Components.getType(clonedBlock?.type || 'default'); const model = type.model; - blockComponent = new model(clonedBlock, opt); + blockComponent = new model( + { + ...clonedBlock, + [keyCollectionsStateMap]: collectionsStateMap, + }, + opt, + ); } const instance = em.Components.addSymbol(blockComponent!); const cmpDefinition = resolveComponent(instance!, block, collectionsStateMap, em); @@ -172,12 +142,13 @@ function resolveComponent( const componentDefinition: ComponentDefinition = { ...componentJSON, components: children, - [collectionsStateMapKey]: collectionsStateMap, + [keyCollectionsStateMap]: collectionsStateMap, }; return componentDefinition; } +// TODO: remove this function function resolveBlockValues(collectionsStateMap: CollectionsStateMap, block: ObjectAny) { const clonedBlock = deepCloneObject(block); const resolvedCollectionValues: ObjectAny = {}; @@ -186,26 +157,12 @@ function resolveBlockValues(collectionsStateMap: CollectionsStateMap, block: Obj const blockKeys = Object.keys(clonedBlock); for (const key of blockKeys) { let blockValue = clonedBlock[key]; - if (key === collectionDefinitionKey) continue; + if (key === keyCollectionDefinition) continue; let hasCollectionVariable = false; if (typeof blockValue === 'object') { - const isCollectionVariable = blockValue.type === CollectionVariableType; + const isCollectionVariable = blockValue.type === 'parent-collection-variable'; if (isCollectionVariable) { - const { - variable_type, - collection_name = innerCollectionStateKey, - path = '', - } = blockValue as CollectionVariable; - const collectionItem = collectionsStateMap[collection_name]; - if (!collectionItem) { - throw new Error(`Collection not found: ${collection_name}`); - } - if (!variable_type) { - throw new Error(`Missing collection variable type for collection: ${collection_name}`); - } - clonedBlock[key] = resolveCurrentItem(variable_type, collectionItem, path); - hasCollectionVariable = true; } else if (Array.isArray(blockValue)) { clonedBlock[key] = blockValue.map((arrayItem: any) => { @@ -241,31 +198,6 @@ function resolveBlockValues(collectionsStateMap: CollectionsStateMap, block: Obj return { clonedBlock, resolvedCollectionValues }; } -function resolveCurrentItem( - variableType: CollectionVariable['variable_type'], - collectionItem: CollectionState, - path: string, -) { - if (variableType === 'current_item') { - const valueIsDataVariable = collectionItem.current_item?.type === DataVariableType; - if (valueIsDataVariable) { - const currentItem_path = collectionItem.current_item.path; - const resolvedPath = currentItem_path ? `${currentItem_path}.${path}` : path; - return { - ...collectionItem.current_item, - path: resolvedPath, - }; - } else if (!!path) { - if (!collectionItem.current_item?.[path]) { - throw new Error(`Path not found in current item: ${path}`); - } - - return collectionItem.current_item[path]; - } - } - return collectionItem[variableType]; -} - function isEmptyObject(itemOverrideKeys: ObjectAny) { return Object.keys(itemOverrideKeys).length === 0; } diff --git a/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts b/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts new file mode 100644 index 000000000..c9ebd1e47 --- /dev/null +++ b/packages/core/src/data_sources/model/collection_component/CollectionVariable.ts @@ -0,0 +1,91 @@ +import { CollectionVariableDefinition } from '../../../../test/specs/dom_components/model/ComponentTypes'; +import { Model } from '../../../common'; +import EditorModel from '../../../editor/model/Editor'; +import DataVariable, { DataVariableType } from '../DataVariable'; +import { keyInnerCollectionState } from './constants'; +import { CollectionsStateMap } from './types'; + +export default class CollectionVariable extends Model { + em: EditorModel; + collectionsStateMap: CollectionsStateMap; + dataVariable?: DataVariable; + + constructor( + attrs: CollectionVariableDefinition, + options: { + em: EditorModel; + collectionsStateMap: CollectionsStateMap; + }, + ) { + super(attrs, options); + this.em = options.em; + this.collectionsStateMap = options.collectionsStateMap; + if (!this.collectionsStateMap) { + throw new Error('collectionsStateMap is required'); + } + + this.updateDataVariable(); + } + + getDataValue() { + const { resolvedValue } = this.updateDataVariable(); + + if (resolvedValue?.type === DataVariableType) { + return this.dataVariable!.getDataValue(); + } + return resolvedValue; + } + + private updateDataVariable() { + const resolvedValue = resolveCollectionVariable( + this.attributes as CollectionVariableDefinition, + this.collectionsStateMap, + ); + + let dataVariable; + if (resolvedValue?.type === DataVariableType) { + dataVariable = new DataVariable(resolvedValue, { em: this.em }); + this.dataVariable = dataVariable; + } + + return { resolvedValue, dataVariable }; + } + + destroy() { + return this.dataVariable?.destroy?.() || super.destroy(); + } +} + +function resolveCollectionVariable( + collectionVariableDefinition: CollectionVariableDefinition, + collectionsStateMap: CollectionsStateMap, +) { + const { collection_name = keyInnerCollectionState, variable_type, path } = collectionVariableDefinition; + const collectionItem = collectionsStateMap[collection_name]; + if (!collectionItem) { + throw new Error(`Collection not found: ${collection_name}`); + } + if (!variable_type) { + throw new Error(`Missing collection variable type for collection: ${collection_name}`); + } + + if (variable_type === 'current_item') { + const valueIsDataVariable = collectionItem.current_item?.type === DataVariableType; + if (valueIsDataVariable) { + const currentItem_path = collectionItem.current_item.path; + const resolvedPath = currentItem_path ? `${currentItem_path}.${path}` : path; + return { + ...collectionItem.current_item, + path: resolvedPath, + }; + } else if (!!path) { + if (!collectionItem.current_item?.[path]) { + throw new Error(`Path not found in current item: ${path} for collection: ${collection_name}`); + } + + return collectionItem.current_item[path]; + } + } + + return collectionItem[variable_type]; +} diff --git a/packages/core/src/data_sources/model/collection_component/constants.ts b/packages/core/src/data_sources/model/collection_component/constants.ts new file mode 100644 index 000000000..adf3b232c --- /dev/null +++ b/packages/core/src/data_sources/model/collection_component/constants.ts @@ -0,0 +1,4 @@ +export const CollectionComponentType = 'collection-component'; +export const keyCollectionDefinition = 'collectionDefinition'; +export const keyInnerCollectionState = 'innerCollectionState'; +export const CollectionVariableType = 'parent-collection-variable'; diff --git a/packages/core/src/data_sources/model/collection_component/types.ts b/packages/core/src/data_sources/model/collection_component/types.ts new file mode 100644 index 000000000..b61abc8af --- /dev/null +++ b/packages/core/src/data_sources/model/collection_component/types.ts @@ -0,0 +1,34 @@ +import { CollectionComponentType } from './constants'; + +import { ComponentDefinition } from '../../../dom_components/model/types'; +import { CollectionVariableDefinition } from '../../../../test/specs/dom_components/model/ComponentTypes'; +import { DataVariableDefinition } from '../DataVariable'; +import { ConditionDefinition } from '../conditional_variables/DataCondition'; + +type CollectionDataSource = any[] | DataVariableDefinition | CollectionVariableDefinition; +type CollectionConfig = { + start_index?: number; + end_index?: number | ConditionDefinition; + dataSource: CollectionDataSource; +}; + +export type CollectionState = { + current_index: number; + start_index: number; + current_item: any; + end_index: number; + collection_name?: string; + total_items: number; + remaining_items: number; +}; + +export type CollectionsStateMap = { + [key: string]: CollectionState; +}; + +export type CollectionDefinition = { + type: typeof CollectionComponentType; + collection_name?: string; + config: CollectionConfig; + block: ComponentDefinition; +}; diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts index 85451ca7f..71322c160 100644 --- a/packages/core/src/data_sources/model/utils.ts +++ b/packages/core/src/data_sources/model/utils.ts @@ -1,10 +1,17 @@ import EditorModel from '../../editor/model/Editor'; import { DynamicValue, DynamicValueDefinition } from '../types'; +import { CollectionsStateMap } from './collection_component/types'; +import CollectionVariable from './collection_component/CollectionVariable'; +import { CollectionVariableDefinition } from '../../../test/specs/dom_components/model/ComponentTypes'; +import { CollectionVariableType } from './collection_component/constants'; import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition'; import DataVariable, { DataVariableType } from './DataVariable'; export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition { - return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value?.type); + return ( + typeof value === 'object' && + [DataVariableType, ConditionalVariableType, CollectionVariableType].includes(value?.type) + ); } export function isDynamicValue(value: any): value is DynamicValue { @@ -23,7 +30,14 @@ export function evaluateVariable(variable: any, em: EditorModel) { return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; } -export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, em: EditorModel): DynamicValue { +export function getDynamicValueInstance( + valueDefinition: DynamicValueDefinition, + options: { + em: EditorModel; + collectionsStateMap?: CollectionsStateMap; + }, +): DynamicValue { + const { em } = options; const dynamicType = valueDefinition.type; let dynamicVariable: DynamicValue; @@ -36,6 +50,11 @@ export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: em }); break; } + case CollectionVariableType: { + // @ts-ignore + dynamicVariable = new CollectionVariable(valueDefinition, options); + break; + } default: throw new Error(`Unsupported dynamic type: ${dynamicType}`); } @@ -43,8 +62,14 @@ export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, return dynamicVariable; } -export function evaluateDynamicValueDefinition(valueDefinition: DynamicValueDefinition, em: EditorModel) { - const dynamicVariable = getDynamicValueInstance(valueDefinition, em); +export function evaluateDynamicValueDefinition( + valueDefinition: DynamicValueDefinition, + options: { + em: EditorModel; + collectionsStateMap?: CollectionsStateMap; + }, +) { + const dynamicVariable = getDynamicValueInstance(valueDefinition, options); return { variable: dynamicVariable, value: dynamicVariable.getDataValue() }; } diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index 95e86123e..f40bf729a 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -1,12 +1,17 @@ import { ObjectAny } from '../common'; +import CollectionVariable from './model/collection_component/CollectionVariable'; +import { CollectionVariableDefinition } from '../../test/specs/dom_components/model/ComponentTypes'; import ComponentDataVariable from './model/ComponentDataVariable'; import DataRecord from './model/DataRecord'; import DataRecords from './model/DataRecords'; import DataVariable, { DataVariableDefinition } from './model/DataVariable'; import { ConditionalVariableDefinition, DataCondition } from './model/conditional_variables/DataCondition'; -export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition; -export type DynamicValueDefinition = DataVariableDefinition | ConditionalVariableDefinition; +export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition | CollectionVariable; +export type DynamicValueDefinition = + | DataVariableDefinition + | ConditionalVariableDefinition + | CollectionVariableDefinition; export interface DataRecordProps extends ObjectAny { /** * Record id. diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index caa45bd98..a5825dea1 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -1,3 +1,4 @@ +import { CollectionsStateMap } from '../../data_sources/model/collection_component/types'; import { isUndefined, isFunction, @@ -53,7 +54,7 @@ import { } from './SymbolUtils'; import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher'; import { DynamicValueWatcher } from './DynamicValueWatcher'; -import { DynamicValueDefinition } from '../../data_sources/types'; +import { CollectionVariableType } from '../../data_sources/model/collection_component/constants'; export interface IComponent extends ExtractMethods {} export interface DynamicWatchersOptions { @@ -75,6 +76,7 @@ export const keySymbol = '__symbol'; export const keySymbolOvrd = '__symbol_ovrd'; export const keyUpdate = ComponentsEvents.update; export const keyUpdateInside = ComponentsEvents.updateInside; +export const keyCollectionsStateMap = '__collections_state_map'; /** * The Component object represents a single node of our template structure, so when you update its properties the changes are @@ -265,8 +267,22 @@ export default class Component extends StyleableModel { componentDVListener: ComponentDynamicValueWatcher; constructor(props: ComponentProperties = {}, opt: ComponentOptions) { - super(props, opt); - this.componentDVListener = new ComponentDynamicValueWatcher(this, opt.em); + if (props[keyCollectionsStateMap]) { + // @ts-ignore + props.components = props.components?.forEach((component) => ({ + ...component, + [keyCollectionsStateMap]: props[keyCollectionsStateMap], + })); + } + super(props, { + ...opt, + // @ts-ignore + [keyCollectionsStateMap]: props[keyCollectionsStateMap], + }); + this.componentDVListener = new ComponentDynamicValueWatcher(this, { + em: opt.em, + collectionsStateMap: props[keyCollectionsStateMap], + }); this.componentDVListener.addProps(props); bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps'); @@ -357,7 +373,9 @@ export default class Component extends StyleableModel { // @ts-ignore const em = this.em || options.em; - const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attributes, em); + // @ts-ignore + const collectionsStateMap = this.get(keyCollectionsStateMap) || options[keyCollectionsStateMap]; + const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attributes, { em, collectionsStateMap }); const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource; if (!shouldSkipWatcherUpdates) { @@ -687,7 +705,9 @@ export default class Component extends StyleableModel { setAttributes(attrs: ObjectAny, opts: SetAttrOptions = { skipWatcherUpdates: false, fromDataSource: false }) { // @ts-ignore const em = this.em || opts.em; - const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attrs, em); + // @ts-ignore + const collectionsStateMap = this.get(keyCollectionsStateMap) || opts[keyCollectionsStateMap]; + const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attrs, { em, collectionsStateMap }); const shouldSkipWatcherUpdates = opts.skipWatcherUpdates || opts.fromDataSource; if (!shouldSkipWatcherUpdates) { this.componentDVListener.setAttributes(attrs); diff --git a/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts b/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts index 911417833..c49cc51cd 100644 --- a/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts +++ b/packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts @@ -1,4 +1,5 @@ import { ObjectAny } from '../../common'; +import { CollectionsStateMap } from '../../data_sources/model/collection_component/types'; import EditorModel from '../../editor/model/Editor'; import Component from './Component'; import { DynamicValueWatcher } from './DynamicValueWatcher'; @@ -9,10 +10,13 @@ export class ComponentDynamicValueWatcher { constructor( private component: Component, - em: EditorModel, + options: { + em: EditorModel; + collectionsStateMap: CollectionsStateMap; + }, ) { - this.propertyWatcher = new DynamicValueWatcher(this.createPropertyUpdater(), em); - this.attributeWatcher = new DynamicValueWatcher(this.createAttributeUpdater(), em); + this.propertyWatcher = new DynamicValueWatcher(this.createPropertyUpdater(), options); + this.attributeWatcher = new DynamicValueWatcher(this.createAttributeUpdater(), options); } private createPropertyUpdater() { diff --git a/packages/core/src/dom_components/model/DynamicValueWatcher.ts b/packages/core/src/dom_components/model/DynamicValueWatcher.ts index 88af44dad..8a3df8a78 100644 --- a/packages/core/src/dom_components/model/DynamicValueWatcher.ts +++ b/packages/core/src/dom_components/model/DynamicValueWatcher.ts @@ -1,3 +1,4 @@ +import { CollectionsStateMap } from '../../data_sources/model/collection_component/types'; import { ObjectAny } from '../../common'; import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; import { evaluateDynamicValueDefinition, isDynamicValueDefinition } from '../../data_sources/model/utils'; @@ -5,13 +6,27 @@ import { DynamicValue } from '../../data_sources/types'; import EditorModel from '../../editor/model/Editor'; export class DynamicValueWatcher { - dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {}; + private dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {}; + private em: EditorModel; + private collectionsStateMap: CollectionsStateMap | undefined; constructor( private updateFn: (key: string, value: any) => void, - private em: EditorModel, - ) {} + options: { + em: EditorModel; + collectionsStateMap?: CollectionsStateMap; + }, + ) { + this.em = options.em; + this.collectionsStateMap = options.collectionsStateMap; + } - static getStaticValues(values: ObjectAny | undefined, em: EditorModel): ObjectAny { + static getStaticValues( + values: ObjectAny | undefined, + options: { + em: EditorModel; + collectionsStateMap?: CollectionsStateMap; + }, + ): ObjectAny { if (!values) return {}; const evaluatedValues: ObjectAny = { ...values }; const propsKeys = Object.keys(values); @@ -20,7 +35,7 @@ export class DynamicValueWatcher { const valueDefinition = values[key]; if (!isDynamicValueDefinition(valueDefinition)) continue; - const { value } = evaluateDynamicValueDefinition(valueDefinition, em); + const { value } = evaluateDynamicValueDefinition(valueDefinition, options); evaluatedValues[key] = value; } @@ -68,7 +83,10 @@ export class DynamicValueWatcher { if (!isDynamicValueDefinition(values[key])) { continue; } - const { variable } = evaluateDynamicValueDefinition(values[key], this.em); + const { variable } = evaluateDynamicValueDefinition(values[key], { + em: this.em, + collectionsStateMap: this.collectionsStateMap, + }); dynamicValues[key] = variable; } diff --git a/packages/core/test/specs/dom_components/model/ComponentTypes.ts b/packages/core/test/specs/dom_components/model/ComponentTypes.ts index 63db8a034..1d240e46a 100644 --- a/packages/core/test/specs/dom_components/model/ComponentTypes.ts +++ b/packages/core/test/specs/dom_components/model/ComponentTypes.ts @@ -1,3 +1,5 @@ +import { CollectionVariableType } from '../../../../src/data_sources/model/collection_component/constants'; +import { CollectionState } from '../../../../src/data_sources/model/collection_component/types'; import Editor from '../../../../src/editor'; describe('Component Types', () => { @@ -96,3 +98,9 @@ describe('Component Types', () => { expect(cmp.components().at(0).is('svg-in')).toBe(true); }); }); +export type CollectionVariableDefinition = { + type: typeof CollectionVariableType; + variable_type: keyof CollectionState; + collection_name?: string; + path?: string; +};