From cc7b2dea2a03e5806a5840dedcb0e1b528858e5f Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 3 Sep 2025 08:48:00 +0300 Subject: [PATCH 01/33] Add tests for component wrapper --- .../dom_components/model/ComponentWrapper.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts index 9706bd3898..0a91db26e4 100644 --- a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts +++ b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts @@ -1,6 +1,11 @@ +import { DataSourceManager, DataSource, DataRecord } from '../../../../src'; +import { DataVariableProps, DataVariableType } from '../../../../src/data_sources/model/DataVariable'; import Component from '../../../../src/dom_components/model/Component'; import ComponentHead from '../../../../src/dom_components/model/ComponentHead'; +import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import Editor from '../../../../src/editor'; +import EditorModel from '../../../../src/editor/model/Editor'; +import { setupTestEditor } from '../../../common'; describe('ComponentWrapper', () => { let em: Editor; @@ -33,4 +38,72 @@ describe('ComponentWrapper', () => { expect(newPageComponent?.head.cid).not.toEqual(originalComponent?.head.cid); }); }); + + describe('ComponentWrapper with DataResolver', () => { + let em: EditorModel; + let dsm: DataSourceManager; + let dataSource: DataSource; + let wrapper: ComponentWrapper; + let firstRecord: DataRecord; + + const records = [ + { + id: 'pages', + data: [ + { id: 'page1', page: 'page1', title: 'Title1', content: 'content 1' }, + { id: 'page2', page: 'page2', title: 'Title2', content: 'content 2' }, + { id: 'page3', page: 'page3', title: 'Title3', content: 'content 3' }, + ], + }, + ]; + + beforeEach(() => { + ({ em, dsm } = setupTestEditor()); + wrapper = em.getWrapper() as ComponentWrapper; + + dataSource = dsm.add({ + id: 'my_data_source_id', + records, + }); + + firstRecord = dataSource.getRecord('page1')!; + }); + + afterEach(() => { + em.destroy(); + }); + + function createDataResolver(path: string): DataVariableProps { + return { + type: DataVariableType, + path, + }; + } + + test('sets dataResolver and updates wrapper.page/head collectionsStateMap', () => { + wrapper.setDataResolver(createDataResolver('my_data_source_id.pages.data')); + const stateMap = wrapper.collectionsStateMap; + + expect(stateMap).toHaveProperty('__pages'); + expect(wrapper.page?.collectionsStateMap).toEqual(stateMap); + expect(wrapper.head.collectionsStateMap).toEqual(stateMap); + }); + + test('children reflect resolved value from dataResolver', () => { + wrapper.setDataResolver(createDataResolver('my_data_source_id.pages.data')); + const child = wrapper.append({ + type: 'default', + })[0]; + expect(child.collectionsStateMap).toEqual(wrapper.collectionsStateMap); + }); + + test('updating record propagates to children', () => { + wrapper.setDataResolver(createDataResolver('my_data_source_id.pages.data')); + const child = wrapper.append({ + type: 'default', + })[0]; + + expect(child.collectionsStateMap).toEqual(wrapper.collectionsStateMap); + }); + }); }); From e7aede43121ce6062d79f370a865b3635efe3979 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 3 Sep 2025 08:48:24 +0300 Subject: [PATCH 02/33] Refactor component data collection --- .../ComponentDataCollection.ts | 173 +++++------------- 1 file changed, 48 insertions(+), 125 deletions(-) diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index 096f09d9ef..cd0ae6484a 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -3,11 +3,9 @@ import { ObjectAny } from '../../../common'; import Component, { keySymbol } from '../../../dom_components/model/Component'; import { ComponentAddType, ComponentDefinitionDefined, ComponentOptions } from '../../../dom_components/model/types'; import EditorModel from '../../../editor/model/Editor'; -import { isObject, toLowerCase } from '../../../utils/mixins'; +import { toLowerCase } from '../../../utils/mixins'; import DataResolverListener from '../DataResolverListener'; -import DataSource from '../DataSource'; -import DataVariable, { DataVariableProps, DataVariableType } from '../DataVariable'; -import { isDataVariable } from '../../utils'; +import { DataVariableProps } from '../DataVariable'; import { DataCollectionItemType, DataCollectionType, keyCollectionDefinition } from './constants'; import { ComponentDataCollectionProps, @@ -17,13 +15,11 @@ import { } from './types'; import { detachSymbolInstance, getSymbolInstances } from '../../../dom_components/model/SymbolUtils'; import { keyDataValues, updateFromWatcher } from '../../../dom_components/model/ModelDataResolverWatchers'; -import { ModelDestroyOptions } from 'backbone'; -import Components from '../../../dom_components/model/Components'; +import ComponentWithCollectionsState, { DataVariableMap } from '../ComponentWithCollectionsState'; const AvoidStoreOptions = { avoidStore: true, partial: true }; -type DataVariableMap = Record; -export default class ComponentDataCollection extends Component { +export default class ComponentDataCollection extends ComponentWithCollectionsState { dataSourceWatcher?: DataResolverListener; get defaults(): ComponentDefinitionDefined { @@ -123,47 +119,59 @@ export default class ComponentDataCollection extends Component { this.firstChild.components(content); } - private get firstChild() { - return this.components().at(0); - } + onCollectionsStateMapUpdate(collectionsStateMap: DataCollectionStateMap) { + super.onCollectionsStateMapUpdate(collectionsStateMap); - private updateCollectionConfig(updates: Partial): void { - this.set(keyCollectionDefinition, { - ...this.dataResolver, - ...updates, + const items = this.getDataSourceItems(); + const { startIndex } = this.resolveCollectionConfig(items); + const cmps = this.components(); + cmps.forEach((cmp, index) => { + const key = this.getItemKey(items, startIndex + index); + const collectionsStateMap = this.getCollectionsStateMapForItem(items, key); + cmp.onCollectionsStateMapUpdate(collectionsStateMap); }); } - private getDataSourceItems() { - const items = getDataSourceItems(this.dataResolver.dataSource, this.em); - if (isArray(items)) { - return items; - } + protected stopSyncComponentCollectionState() { + this.stopListening(this.components(), 'add remove reset', this.syncOnComponentChange); + this.onCollectionsStateMapUpdate({}); + } + + protected setCollectionStateMapAndPropagate(cmp: Component, collectionsStateMap: DataCollectionStateMap) { + cmp.setSymbolOverride(['locked', 'layerable', keyDataValues]); + cmp.syncComponentsCollectionState(); + cmp.onCollectionsStateMapUpdate(collectionsStateMap); + } - const clone = { ...items }; - delete clone['__p']; - return clone; + protected onDataSourceChange() { + this.rebuildChildrenFromCollection(); } - private get dataResolver() { - return (this.get(keyCollectionDefinition) || {}) as DataCollectionProps; + protected listenToPropsChange() { + this.on(`change:${keyCollectionDefinition}`, () => { + this.rebuildChildrenFromCollection(); + this.listenToDataSource(); + }); + + this.listenToDataSource(); } - private get collectionDataSource() { + protected get dataSourceProps(): DataVariableProps | undefined { return this.dataResolver.dataSource; } - private listenToDataSource() { - const { em } = this; - const path = this.collectionDataSource?.path; - if (!path) return; - this.dataSourceWatcher = new DataResolverListener({ - em, - resolver: new DataVariable( - { type: DataVariableType, path }, - { em, collectionsStateMap: this.collectionsStateMap }, - ), - onUpdate: this.rebuildChildrenFromCollection, + protected get dataResolver(): DataCollectionProps { + return this.get(keyCollectionDefinition) || {}; + } + + private get firstChild() { + return this.components().at(0); + } + + private updateCollectionConfig(updates: Partial): void { + this.set(keyCollectionDefinition, { + ...this.dataResolver, + ...updates, }); } @@ -200,13 +208,13 @@ export default class ComponentDataCollection extends Component { for (let index = startIndex; index <= endIndex; index++) { const isFirstItem = index === startIndex; - const key = isArray(items) ? index : Object.keys(items)[index]; + const key = this.getItemKey(items, index); const collectionsStateMap = this.getCollectionsStateMapForItem(items, key); if (isFirstItem) { getSymbolInstances(firstChild)?.forEach((cmp) => detachSymbolInstance(cmp)); - setCollectionStateMapAndPropagate(firstChild, collectionsStateMap); + this.setCollectionStateMapAndPropagate(firstChild, collectionsStateMap); // TODO: Move to component view firstChild.addStyle({ display: resolvedDisplay }, AvoidStoreOptions); @@ -215,7 +223,7 @@ export default class ComponentDataCollection extends Component { const instance = firstChild!.clone({ symbol: true, symbolInv: true }); instance.set({ locked: true, layerable: false }, AvoidStoreOptions); - setCollectionStateMapAndPropagate(instance, collectionsStateMap); + this.setCollectionStateMapAndPropagate(instance, collectionsStateMap); components.push(instance); } @@ -287,46 +295,6 @@ export default class ComponentDataCollection extends Component { ); } - private listenToPropsChange() { - this.on(`change:${keyCollectionDefinition}`, () => { - this.rebuildChildrenFromCollection(); - this.listenToDataSource(); - }); - this.listenToDataSource(); - } - - private removePropsListeners() { - this.off(`change:${keyCollectionDefinition}`); - this.dataSourceWatcher?.destroy(); - } - - onCollectionsStateMapUpdate(collectionsStateMap: DataCollectionStateMap) { - super.onCollectionsStateMapUpdate(collectionsStateMap); - - const items = this.getDataSourceItems(); - const { startIndex } = this.resolveCollectionConfig(items); - const cmps = this.components(); - cmps.forEach((cmp, index) => { - const collectionsStateMap = this.getCollectionsStateMapForItem(items, startIndex + index); - cmp.onCollectionsStateMapUpdate(collectionsStateMap); - }); - } - - stopSyncComponentCollectionState() { - this.stopListening(this.components(), 'add remove reset', this.syncOnComponentChange); - this.onCollectionsStateMapUpdate({}); - } - - syncOnComponentChange(model: Component, collection: Components, opts: any) { - const collectionsStateMap = this.collectionsStateMap; - // Avoid assigning wrong collectionsStateMap value to children components - this.collectionsStateMap = {}; - - super.syncOnComponentChange(model, collection, opts); - this.collectionsStateMap = collectionsStateMap; - this.onCollectionsStateMapUpdate(collectionsStateMap); - } - private get collectionId() { return this.getDataResolver().collectionId as string; } @@ -344,23 +312,12 @@ export default class ComponentDataCollection extends Component { const firstChild = this.firstChild as any; return { ...json, components: [firstChild] }; } - - destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { - this.removePropsListeners(); - return super.destroy(options); - } } function getLength(items: DataVariableProps[] | object) { return isArray(items) ? items.length : Object.keys(items).length; } -function setCollectionStateMapAndPropagate(cmp: Component, collectionsStateMap: DataCollectionStateMap) { - cmp.setSymbolOverride(['locked', 'layerable', keyDataValues]); - cmp.syncComponentsCollectionState(); - cmp.onCollectionsStateMapUpdate(collectionsStateMap); -} - function logErrorIfMissing(property: any, propertyPath: string, em: EditorModel) { if (!property) { em.logError(`The "${propertyPath}" property is required in the collection definition.`); @@ -389,37 +346,3 @@ function validateCollectionDef(dataResolver: DataCollectionProps, em: EditorMode return true; } - -function getDataSourceItems( - dataSource: DataCollectionDataSource, - em: EditorModel, -): DataVariableProps[] | DataVariableMap { - switch (true) { - case isObject(dataSource) && dataSource instanceof DataSource: { - const id = dataSource.get('id')!; - return listDataSourceVariables(id, em); - } - case isDataVariable(dataSource): { - const path = dataSource.path; - if (!path) return []; - const isDataSourceId = path.split('.').length === 1; - if (isDataSourceId) { - return listDataSourceVariables(path, em); - } else { - return em.DataSources.getValue(path, []); - } - } - default: - return []; - } -} - -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, - })); -} From d81686d476fb71c882fc0744c555c8a8d2375249 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 3 Sep 2025 08:48:41 +0300 Subject: [PATCH 03/33] Add data resolver to wrapper component --- .../model/ComponentWithCollectionsState.ts | 139 ++++++++++++++++++ .../dom_components/model/ComponentWrapper.ts | 102 ++++++++++++- 2 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/data_sources/model/ComponentWithCollectionsState.ts diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts new file mode 100644 index 0000000000..e4cc64839d --- /dev/null +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -0,0 +1,139 @@ +import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; +import DataResolverListener from '../../data_sources/model/DataResolverListener'; +import DataVariable, { DataVariableProps, DataVariableType } from '../../data_sources/model/DataVariable'; +import Components from '../../dom_components/model/Components'; +import Component from '../../dom_components/model/Component'; +import { ObjectAny } from '../../common'; +import { isDataVariable } from '../utils'; +import { isObject } from '../../utils/mixins'; +import DataSource from './DataSource'; +import { isArray } from 'underscore'; + +export type DataVariableMap = Record; + +export default class ComponentWithCollectionsState extends Component { + collectionsStateMap: DataCollectionStateMap = {}; + dataSourceWatcher?: DataResolverListener; + + constructor(props: any, opt: any) { + super(props, opt); + this.listenToPropsChange(); + } + + onCollectionsStateMapUpdate(collectionsStateMap: DataCollectionStateMap) { + this.collectionsStateMap = collectionsStateMap; + this.dataResolverWatchers?.onCollectionsStateMapUpdate?.(); + + this.components().forEach((cmp) => { + cmp.onCollectionsStateMapUpdate?.(collectionsStateMap); + }); + } + + syncOnComponentChange(model: Component, collection: Components, opts: any) { + const prev = this.collectionsStateMap; + this.collectionsStateMap = {}; + super.syncOnComponentChange(model, collection, opts); + this.collectionsStateMap = prev; + this.onCollectionsStateMapUpdate(prev); + } + + syncComponentsCollectionState() { + super.syncComponentsCollectionState(); + this.components().forEach((cmp) => cmp.syncComponentsCollectionState?.()); + } + + protected listenToDataSource() { + const path = this.dataSourcePath; + if (!path) return; + + const { em } = this; + this.dataSourceWatcher = new DataResolverListener({ + em, + resolver: new DataVariable( + { type: DataVariableType, path }, + { em, collectionsStateMap: this.collectionsStateMap }, + ), + onUpdate: () => this.onDataSourceChange(), + }); + } + + protected listenToPropsChange() { + this.on(`change:dataResolver`, () => { + this.listenToDataSource(); + }); + + this.listenToDataSource(); + } + + protected get dataSourceProps(): DataVariableProps | undefined { + return this.get('dataResolver'); + } + + protected get dataSourcePath(): string | undefined { + return this.dataSourceProps?.path; + } + + protected onDataSourceChange() { + this.onCollectionsStateMapUpdate(this.collectionsStateMap); + } + + protected getDataSourceItems() { + const dataSourceProps = this.dataSourceProps; + if (!dataSourceProps) return []; + const items = this.listDataSourceItems(dataSourceProps); + if (items && isArray(items)) { + return items; + } + + const clone = { ...items }; + delete clone['__p']; + return clone; + } + + protected listDataSourceItems(dataSource: DataSource | DataVariableProps): DataVariableProps[] | DataVariableMap { + const em = this.em; + switch (true) { + case isObject(dataSource) && dataSource instanceof DataSource: { + const id = dataSource.get('id')!; + return this.listDataSourceVariables(id); + } + case isDataVariable(dataSource): { + const path = dataSource.path; + if (!path) return []; + const isDataSourceId = path.split('.').length === 1; + if (isDataSourceId) { + return this.listDataSourceVariables(path); + } else { + return em.DataSources.getValue(path, []); + } + } + default: + return []; + } + } + + protected getItemKey(items: DataVariableProps[] | { [x: string]: DataVariableProps }, index: number) { + return isArray(items) ? index : Object.keys(items)[index]; + } + + private removePropsListeners() { + this.off(`change:dataResolver`); + this.dataSourceWatcher?.destroy(); + this.dataSourceWatcher = undefined; + } + + private listDataSourceVariables(path: string): DataVariableProps[] { + const records = this.em.DataSources.getValue(path, []); + const keys = Object.keys(records); + + return keys.map((key) => ({ + type: DataVariableType, + path: path + '.' + key, + })); + } + + destroy(options?: ObjectAny): false | JQueryXHR { + this.removePropsListeners(); + return super.destroy(options); + } +} diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index d32080d742..9431ff9e8f 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -1,10 +1,17 @@ -import { isUndefined } from 'underscore'; +import { isArray, isUndefined } from 'underscore'; import { attrToString } from '../../utils/dom'; import Component from './Component'; import ComponentHead, { type as typeHead } from './ComponentHead'; import { ToHTMLOptions } from './types'; +import Components from './Components'; +import DataResolverListener from '../../data_sources/model/DataResolverListener'; +import { DataVariableProps } from '../../data_sources/model/DataVariable'; +import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; +import ComponentWithCollectionsState, { DataVariableMap } from '../../data_sources/model/ComponentWithCollectionsState'; + +export default class ComponentWrapper extends ComponentWithCollectionsState { + dataSourceWatcher?: DataResolverListener; -export default class ComponentWrapper extends Component { get defaults() { return { // @ts-ignore @@ -30,6 +37,21 @@ export default class ComponentWrapper extends Component { }; } + constructor(props: any, opt: any) { + super(props, opt); + + const hasDataResolver = this.getDataResolver(); + console.log('🚀 ~ ComponentWrapper ~ constructor ~ hasDataResolver:', hasDataResolver); + if (hasDataResolver) { + this.syncComponentsCollectionState(); + console.log( + '🚀 ~ ComponentWrapper ~ constructor ~ this.getCollectionsStateMap():', + this.getCollectionsStateMap(), + ); + this.onCollectionsStateMapUpdate(this.getCollectionsStateMap()); + } + } + preInit() { const { opt, attributes: props } = this; const cmp = this.em?.Components; @@ -78,6 +100,82 @@ export default class ComponentWrapper extends Component { return asDoc ? `${doctype}${headStr}${body}` : body; } + setDataResolver(dataResolver: DataVariableProps) { + return this.set('dataResolver', dataResolver); + } + + getDataResolver() { + return this.get('dataResolver'); + } + + onCollectionsStateMapUpdate(collectionsStateMap: DataCollectionStateMap) { + const { page, head } = this; + super.onCollectionsStateMapUpdate(collectionsStateMap); + head.onCollectionsStateMapUpdate(collectionsStateMap); + if (page) page.collectionsStateMap = collectionsStateMap; + } + + syncComponentsCollectionState() { + super.syncComponentsCollectionState(); + this.head.syncComponentsCollectionState(); + } + + syncOnComponentChange(model: Component, collection: Components, opts: any) { + const collectionsStateMap: any = this.getCollectionsStateMap(); + + this.collectionsStateMap = collectionsStateMap; + super.syncOnComponentChange(model, collection, opts); + this.onCollectionsStateMapUpdate(collectionsStateMap); + } + + protected listenToPropsChange() { + this.on(`change:dataResolver`, () => { + this.onCollectionsStateMapUpdate(this.getCollectionsStateMap()); + this.listenToDataSource(); + }); + + this.listenToDataSource(); + } + + private getCollectionsStateMapForItem(items: DataVariableProps[] | DataVariableMap, key: number | string) { + const totalItems = Object.keys(items).length; + let item: DataVariableProps = (items as any)[key]; + const numericKey = typeof key === 'string' ? Object.keys(items).indexOf(key) : key; + const offset = numericKey - 0; + const remainingItems = totalItems - (1 + offset); + const collectionState = { + collectionId: '__pages', + currentIndex: numericKey, + currentItem: item, + currentKey: key, + startIndex: 0, + endIndex: totalItems - 1, + totalItems: totalItems, + remainingItems, + }; + + return collectionState; + } + + private getCollectionsStateMap(): DataCollectionStateMap { + const path = this.dataSourcePath; + if (!path) return this.collectionsStateMap; + const items = this.getDataSourceItems(); + const pages = []; + const length = Object.keys(items).length; + for (let index = 0; index <= length; index++) { + const key = this.getItemKey(items, index); + const collectionsStateMap = this.getCollectionsStateMapForItem(items, key); + pages.push(collectionsStateMap); + } + + const collectionsStateMap = { + __pages: { currentItem: pages }, + }; + + return collectionsStateMap as any; + } + __postAdd() { const um = this.em?.UndoManager; !this.__hasUm && um?.add(this); From 0cb1a839cb98e93f2abbac00a8c5de488edef158 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 3 Sep 2025 08:48:51 +0300 Subject: [PATCH 04/33] Fix types --- .../core/src/data_sources/model/data_collection/types.ts | 6 ++++-- packages/core/src/dom_components/model/Component.ts | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/src/data_sources/model/data_collection/types.ts b/packages/core/src/data_sources/model/data_collection/types.ts index ea1bf324d6..2d9b82bfa9 100644 --- a/packages/core/src/data_sources/model/data_collection/types.ts +++ b/packages/core/src/data_sources/model/data_collection/types.ts @@ -26,9 +26,11 @@ export interface DataCollectionState { [DataCollectionStateType.remainingItems]: number; } -export interface DataCollectionStateMap { +export type DataCollectionStateMap = { [key: string]: DataCollectionState; -} +} & { + __pages?: { [DataCollectionStateType.currentItem]: DataCollectionState }; +}; export interface ComponentDataCollectionProps extends ComponentDefinition { type: typeof DataCollectionType; diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 1dae41ba42..fe4af01580 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -364,13 +364,13 @@ export default class Component extends StyleableModel { this.components().forEach((cmp) => cmp.syncComponentsCollectionState()); } - stopSyncComponentCollectionState() { + protected stopSyncComponentCollectionState() { this.stopListening(this.components(), 'add remove reset', this.syncOnComponentChange); this.collectionsStateMap = {}; this.components().forEach((cmp) => cmp.stopSyncComponentCollectionState()); } - syncOnComponentChange(model: Component, collection: Components, opts: any) { + protected syncOnComponentChange(model: Component, collection: Components, opts: any) { if (!this.collectionsStateMap || !Object.keys(this.collectionsStateMap).length) return; const options = opts || collection || {}; From af2c99f11859f9942441a408df0002c239069009 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 3 Sep 2025 08:49:01 +0300 Subject: [PATCH 05/33] Add collection data source to page --- packages/core/src/pages/model/Page.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/core/src/pages/model/Page.ts b/packages/core/src/pages/model/Page.ts index 03d8f3d390..5ab63bb731 100644 --- a/packages/core/src/pages/model/Page.ts +++ b/packages/core/src/pages/model/Page.ts @@ -6,6 +6,7 @@ import ComponentWrapper from '../../dom_components/model/ComponentWrapper'; import EditorModel from '../../editor/model/Editor'; import { CssRuleJSON } from '../../css_composer/model/CssRule'; import { ComponentDefinition } from '../../dom_components/model/types'; +import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; /** @private */ export interface PageProperties { @@ -38,6 +39,8 @@ export interface PagePropertiesDefined extends Pick { + collectionsStateMap: DataCollectionStateMap = {}; + defaults() { return { name: '', From 436c06406ddc768c033fdc536dfddd46c5b8845e Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 8 Sep 2025 14:44:59 +0300 Subject: [PATCH 06/33] refactor get and set DataResolver to componentWrapper --- .../model/ComponentWithCollectionsState.ts | 10 +++++++++- .../data_collection/ComponentDataCollection.ts | 12 ++---------- .../src/dom_components/model/ComponentWrapper.ts | 16 ++-------------- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts index e4cc64839d..bcb90c511a 100644 --- a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -11,7 +11,7 @@ import { isArray } from 'underscore'; export type DataVariableMap = Record; -export default class ComponentWithCollectionsState extends Component { +export default class ComponentWithCollectionsState extends Component { collectionsStateMap: DataCollectionStateMap = {}; dataSourceWatcher?: DataResolverListener; @@ -42,6 +42,14 @@ export default class ComponentWithCollectionsState extends Component { this.components().forEach((cmp) => cmp.syncComponentsCollectionState?.()); } + setDataResolver(dataResolver: DataResolverType | undefined) { + return this.set('dataResolver', dataResolver); + } + + getDataResolver(): DataResolverType | undefined { + return this.get('dataResolver'); + } + protected listenToDataSource() { const path = this.dataSourcePath; if (!path) return; diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index cd0ae6484a..600301a530 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -19,7 +19,7 @@ import ComponentWithCollectionsState, { DataVariableMap } from '../ComponentWith const AvoidStoreOptions = { avoidStore: true, partial: true }; -export default class ComponentDataCollection extends ComponentWithCollectionsState { +export default class ComponentDataCollection extends ComponentWithCollectionsState { dataSourceWatcher?: DataResolverListener; get defaults(): ComponentDefinitionDefined { @@ -51,10 +51,6 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta return cmp; } - getDataResolver() { - return this.get('dataResolver'); - } - getItemsCount() { const items = this.getDataSourceItems(); const itemsCount = getLength(items); @@ -87,10 +83,6 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta return this.firstChild.components(); } - setDataResolver(props: DataCollectionProps) { - return this.set('dataResolver', props); - } - setCollectionId(collectionId: string) { this.updateCollectionConfig({ collectionId }); } @@ -296,7 +288,7 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta } private get collectionId() { - return this.getDataResolver().collectionId as string; + return this.getDataResolver()?.collectionId ?? ''; } static isComponent(el: HTMLElement) { diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index 9431ff9e8f..06a3b21c2e 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -9,7 +9,7 @@ import { DataVariableProps } from '../../data_sources/model/DataVariable'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import ComponentWithCollectionsState, { DataVariableMap } from '../../data_sources/model/ComponentWithCollectionsState'; -export default class ComponentWrapper extends ComponentWithCollectionsState { +export default class ComponentWrapper extends ComponentWithCollectionsState { dataSourceWatcher?: DataResolverListener; get defaults() { @@ -41,13 +41,9 @@ export default class ComponentWrapper extends ComponentWithCollectionsState { super(props, opt); const hasDataResolver = this.getDataResolver(); - console.log('🚀 ~ ComponentWrapper ~ constructor ~ hasDataResolver:', hasDataResolver); + if (hasDataResolver) { this.syncComponentsCollectionState(); - console.log( - '🚀 ~ ComponentWrapper ~ constructor ~ this.getCollectionsStateMap():', - this.getCollectionsStateMap(), - ); this.onCollectionsStateMapUpdate(this.getCollectionsStateMap()); } } @@ -100,14 +96,6 @@ export default class ComponentWrapper extends ComponentWithCollectionsState { return asDoc ? `${doctype}${headStr}${body}` : body; } - setDataResolver(dataResolver: DataVariableProps) { - return this.set('dataResolver', dataResolver); - } - - getDataResolver() { - return this.get('dataResolver'); - } - onCollectionsStateMapUpdate(collectionsStateMap: DataCollectionStateMap) { const { page, head } = this; super.onCollectionsStateMapUpdate(collectionsStateMap); From 59096ea78bf06e3a4c2178fe197ee4f8b8e2cd89 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 8 Sep 2025 14:55:30 +0300 Subject: [PATCH 07/33] Rename key to __rootData --- packages/core/src/dom_components/model/ComponentWrapper.ts | 5 ++++- .../core/test/specs/dom_components/model/ComponentWrapper.ts | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index 06a3b21c2e..de29df06ec 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -9,6 +9,9 @@ import { DataVariableProps } from '../../data_sources/model/DataVariable'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import ComponentWithCollectionsState, { DataVariableMap } from '../../data_sources/model/ComponentWithCollectionsState'; +export const keyRootData = '__rootData'; + + export default class ComponentWrapper extends ComponentWithCollectionsState { dataSourceWatcher?: DataResolverListener; @@ -132,7 +135,7 @@ export default class ComponentWrapper extends ComponentWithCollectionsState { wrapper.setDataResolver(createDataResolver('my_data_source_id.pages.data')); const stateMap = wrapper.collectionsStateMap; - expect(stateMap).toHaveProperty('__pages'); + expect(stateMap).toHaveProperty(keyRootData); expect(wrapper.page?.collectionsStateMap).toEqual(stateMap); expect(wrapper.head.collectionsStateMap).toEqual(stateMap); }); From 7df0c9f9a86939936e5e4a5370eed296f1f7512a Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 8 Sep 2025 23:20:17 +0300 Subject: [PATCH 08/33] add resolverCurrentItem --- .../model/ComponentWithCollectionsState.ts | 4 +- .../src/data_sources/model/DataVariable.ts | 39 +++++++---- .../model/data_collection/types.ts | 6 +- packages/core/src/dom_components/constants.ts | 1 + .../dom_components/model/ComponentWrapper.ts | 70 ++++++++----------- .../dom_components/model/ComponentWrapper.ts | 3 +- 6 files changed, 69 insertions(+), 54 deletions(-) create mode 100644 packages/core/src/dom_components/constants.ts diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts index bcb90c511a..ea308df5e3 100644 --- a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -11,6 +11,8 @@ import { isArray } from 'underscore'; export type DataVariableMap = Record; +export type DataSourceRecords = DataVariableProps[] | DataVariableMap; + export default class ComponentWithCollectionsState extends Component { collectionsStateMap: DataCollectionStateMap = {}; dataSourceWatcher?: DataResolverListener; @@ -98,7 +100,7 @@ export default class ComponentWithCollectionsState extends Com return clone; } - protected listDataSourceItems(dataSource: DataSource | DataVariableProps): DataVariableProps[] | DataVariableMap { + protected listDataSourceItems(dataSource: DataSource | DataVariableProps): DataSourceRecords { const em = this.em; switch (true) { case isObject(dataSource) && dataSource instanceof DataSource: { diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index c7f195fcca..e34318e014 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -1,7 +1,13 @@ import { Model } from '../../common'; +import { keyRootData } from '../../dom_components/constants'; import EditorModel from '../../editor/model/Editor'; import { isDataVariable } from '../utils'; -import { DataCollectionStateMap, DataCollectionState, DataCollectionStateType } from './data_collection/types'; +import { + DataCollectionStateMap, + DataCollectionState, + DataCollectionStateType, + RootDataType, +} from './data_collection/types'; export const DataVariableType = 'data-variable' as const; @@ -134,36 +140,45 @@ export default class DataVariable extends Model { ); } - private resolveCollectionVariable(): unknown { + private resolveCollectionVariable() { const { em, collectionsStateMap } = this; return DataVariable.resolveCollectionVariable(this.attributes, { em, collectionsStateMap }); } static resolveCollectionVariable( - dataResolverProps: { + { + collectionId = '', + variableType, + path, + defaultValue = '', + }: { collectionId?: string; variableType?: DataCollectionStateType; path?: string; defaultValue?: string; }, - opts: DataVariableOptions, - ): unknown { - const { collectionId = '', variableType, path, defaultValue = '' } = dataResolverProps; - const { em, collectionsStateMap } = opts; - + { em, collectionsStateMap }: DataVariableOptions, + ) { if (!collectionsStateMap) return defaultValue; const collectionItem = collectionsStateMap[collectionId]; if (!collectionItem) return defaultValue; if (!variableType) { + if (collectionId === keyRootData) { + const rootData = collectionItem as RootDataType; + return path ? rootData[path as keyof RootDataType] : rootData; + } + em.logError(`Missing collection variable type for collection: ${collectionId}`); return defaultValue; } - return variableType === 'currentItem' - ? DataVariable.resolveCurrentItem(collectionItem, path, collectionId, em) - : collectionItem[variableType]; + if (variableType === 'currentItem') { + return DataVariable.resolveCurrentItem(collectionItem, path, collectionId, em); + } + + return collectionItem[variableType] ?? defaultValue; } private static resolveCurrentItem( @@ -171,7 +186,7 @@ export default class DataVariable extends Model { path: string | undefined, collectionId: string, em: EditorModel, - ): unknown { + ) { const currentItem = collectionItem.currentItem; if (!currentItem) { em.logError(`Current item is missing for collection: ${collectionId}`); diff --git a/packages/core/src/data_sources/model/data_collection/types.ts b/packages/core/src/data_sources/model/data_collection/types.ts index 2d9b82bfa9..c050cf8f11 100644 --- a/packages/core/src/data_sources/model/data_collection/types.ts +++ b/packages/core/src/data_sources/model/data_collection/types.ts @@ -1,6 +1,8 @@ import { DataCollectionType, keyCollectionDefinition } from './constants'; import { ComponentDefinition } from '../../../dom_components/model/types'; import { DataVariableProps } from '../DataVariable'; +import { keyRootData } from '../../../dom_components/constants'; +import { ObjectAny } from '../../../common'; export type DataCollectionDataSource = DataVariableProps; @@ -26,10 +28,12 @@ export interface DataCollectionState { [DataCollectionStateType.remainingItems]: number; } +export type RootDataType = Array | ObjectAny; + export type DataCollectionStateMap = { [key: string]: DataCollectionState; } & { - __pages?: { [DataCollectionStateType.currentItem]: DataCollectionState }; + [keyRootData]?: RootDataType; }; export interface ComponentDataCollectionProps extends ComponentDefinition { diff --git a/packages/core/src/dom_components/constants.ts b/packages/core/src/dom_components/constants.ts new file mode 100644 index 0000000000..6b5aa1bbc1 --- /dev/null +++ b/packages/core/src/dom_components/constants.ts @@ -0,0 +1 @@ +export const keyRootData = '__rootData'; diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index de29df06ec..83b5557a83 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -1,19 +1,22 @@ -import { isArray, isUndefined } from 'underscore'; +import { isUndefined } from 'underscore'; import { attrToString } from '../../utils/dom'; import Component from './Component'; import ComponentHead, { type as typeHead } from './ComponentHead'; -import { ToHTMLOptions } from './types'; +import { ComponentOptions, ComponentProperties, ToHTMLOptions } from './types'; import Components from './Components'; import DataResolverListener from '../../data_sources/model/DataResolverListener'; import { DataVariableProps } from '../../data_sources/model/DataVariable'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; -import ComponentWithCollectionsState, { DataVariableMap } from '../../data_sources/model/ComponentWithCollectionsState'; - -export const keyRootData = '__rootData'; +import ComponentWithCollectionsState, { + DataSourceRecords, +} from '../../data_sources/model/ComponentWithCollectionsState'; +import { keyRootData } from '../constants'; +type ResolverCurrentItemType = string | number | undefined; export default class ComponentWrapper extends ComponentWithCollectionsState { dataSourceWatcher?: DataResolverListener; + _resolverCurrentItem: ResolverCurrentItemType; get defaults() { return { @@ -40,13 +43,13 @@ export default class ComponentWrapper extends ComponentWithCollectionsState { this.onCollectionsStateMapUpdate(this.getCollectionsStateMap()); @@ -128,43 +140,23 @@ export default class ComponentWrapper extends ComponentWithCollectionsState Date: Tue, 9 Sep 2025 01:10:41 +0300 Subject: [PATCH 09/33] Make _resolverCurrentItem private --- packages/core/src/dom_components/model/ComponentWrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index 83b5557a83..debfba3b50 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -16,7 +16,7 @@ type ResolverCurrentItemType = string | number | undefined; export default class ComponentWrapper extends ComponentWithCollectionsState { dataSourceWatcher?: DataResolverListener; - _resolverCurrentItem: ResolverCurrentItemType; + private _resolverCurrentItem: ResolverCurrentItemType; get defaults() { return { From 1bf25c043a833801f62c7644991d374cc8bb925b Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Tue, 9 Sep 2025 01:31:18 +0300 Subject: [PATCH 10/33] update ComponentWrapper tests --- .../dom_components/model/ComponentWrapper.ts | 80 +++++++++++-------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts index 917829aebe..fa7661b672 100644 --- a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts +++ b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts @@ -43,46 +43,57 @@ describe('ComponentWrapper', () => { describe('ComponentWrapper with DataResolver', () => { let em: EditorModel; let dsm: DataSourceManager; - let dataSource: DataSource; + let pagesDataSource: DataSource; + let flatPagesDataSource: DataSource; let wrapper: ComponentWrapper; - let firstRecord: DataRecord; - - const records = [ - { - id: 'pages', - data: [ - { id: 'page1', page: 'page1', title: 'Title1', content: 'content 1' }, - { id: 'page2', page: 'page2', title: 'Title2', content: 'content 2' }, - { id: 'page3', page: 'page3', title: 'Title3', content: 'content 3' }, - ], - }, + + const pagesData = [ + { id: 'page1', page: 'page1', title: 'Title1', content: 'content 1' }, + { id: 'page2', page: 'page2', title: 'Title2', content: 'content 2' }, + { id: 'page3', page: 'page3', title: 'Title3', content: 'content 3' }, ]; beforeEach(() => { ({ em, dsm } = setupTestEditor()); wrapper = em.getWrapper() as ComponentWrapper; - dataSource = dsm.add({ - id: 'my_data_source_id', - records, + pagesDataSource = dsm.add({ + id: 'pagesDataSource', + records: [{ id: 'pages', data: pagesData }], }); - firstRecord = dataSource.getRecord('page1')!; + flatPagesDataSource = dsm.add({ + id: 'flatPagesDataSource', + records: pagesData, + }); }); afterEach(() => { em.destroy(); }); - function createDataResolver(path: string): DataVariableProps { - return { - type: DataVariableType, - path, - }; - } + const createDataResolver = (path: string): DataVariableProps => ({ + type: DataVariableType, + path, + }); + + const setResolver = (path: string) => { + wrapper.setDataResolver(createDataResolver(path)); + wrapper.resolverCurrentItem = 0; + }; + + const appendChildWithTitle = () => + wrapper.append({ + type: 'default', + title: { + type: 'data-variable', + collectionId: keyRootData, + path: 'title', + }, + })[0]; test('sets dataResolver and updates wrapper.page/head collectionsStateMap', () => { - wrapper.setDataResolver(createDataResolver('my_data_source_id.pages.data')); + setResolver('pagesDataSource.pages.data'); const stateMap = wrapper.collectionsStateMap; expect(stateMap).toHaveProperty(keyRootData); @@ -91,20 +102,25 @@ describe('ComponentWrapper', () => { }); test('children reflect resolved value from dataResolver', () => { - wrapper.setDataResolver(createDataResolver('my_data_source_id.pages.data')); - const child = wrapper.append({ - type: 'default', - })[0]; + setResolver('pagesDataSource.pages.data'); + const child = appendChildWithTitle(); + expect(child.collectionsStateMap).toEqual(wrapper.collectionsStateMap); + expect(child.collectionsStateMap).toEqual({ + [keyRootData]: pagesData[0], + }); + expect(child.get('title')).toBe(pagesData[0].title); }); - test('updating record propagates to children', () => { - wrapper.setDataResolver(createDataResolver('my_data_source_id.pages.data')); - const child = wrapper.append({ - type: 'default', - })[0]; + test('updating record propagates to children2', () => { + setResolver('flatPagesDataSource'); + const child = appendChildWithTitle(); expect(child.collectionsStateMap).toEqual(wrapper.collectionsStateMap); + expect(child.collectionsStateMap).toEqual({ + [keyRootData]: pagesData[0], + }); + expect(child.get('title')).toBe(pagesData[0].title); }); }); }); From b6181addd9166417474ed688372711b0eea4f443 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Tue, 9 Sep 2025 01:31:32 +0300 Subject: [PATCH 11/33] Fix componentWithCollectionsState --- .../model/ComponentWithCollectionsState.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts index ea308df5e3..e9db448b75 100644 --- a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -105,14 +105,14 @@ export default class ComponentWithCollectionsState extends Com switch (true) { case isObject(dataSource) && dataSource instanceof DataSource: { const id = dataSource.get('id')!; - return this.listDataSourceVariables(id); + return this.getDataSourceRecordsItems(id); } case isDataVariable(dataSource): { const path = dataSource.path; if (!path) return []; const isDataSourceId = path.split('.').length === 1; if (isDataSourceId) { - return this.listDataSourceVariables(path); + return this.getDataSourceRecordsItems(path); } else { return em.DataSources.getValue(path, []); } @@ -132,14 +132,10 @@ export default class ComponentWithCollectionsState extends Com this.dataSourceWatcher = undefined; } - private listDataSourceVariables(path: string): DataVariableProps[] { + private getDataSourceRecordsItems(path: string): any[] { const records = this.em.DataSources.getValue(path, []); - const keys = Object.keys(records); - return keys.map((key) => ({ - type: DataVariableType, - path: path + '.' + key, - })); + return Object.entries(records).map(([_, value]) => value); } destroy(options?: ObjectAny): false | JQueryXHR { From 608fdd9d3e304baa826723c56c261a769896c053 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Tue, 9 Sep 2025 01:32:43 +0300 Subject: [PATCH 12/33] remove collectionsStateMap from Page --- packages/core/src/dom_components/model/ComponentWrapper.ts | 3 +-- packages/core/src/pages/model/Page.ts | 2 -- .../core/test/specs/dom_components/model/ComponentWrapper.ts | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index debfba3b50..2063b3c540 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -103,10 +103,9 @@ export default class ComponentWrapper extends ComponentWithCollectionsState { - collectionsStateMap: DataCollectionStateMap = {}; - defaults() { return { name: '', diff --git a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts index fa7661b672..dfa0ed5392 100644 --- a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts +++ b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts @@ -97,7 +97,6 @@ describe('ComponentWrapper', () => { const stateMap = wrapper.collectionsStateMap; expect(stateMap).toHaveProperty(keyRootData); - expect(wrapper.page?.collectionsStateMap).toEqual(stateMap); expect(wrapper.head.collectionsStateMap).toEqual(stateMap); }); From 9e68c92148130848b67cc2e67411c8046a8497c1 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Tue, 9 Sep 2025 20:28:18 +0300 Subject: [PATCH 13/33] update component wrapper tests --- .../dom_components/model/ComponentWrapper.ts | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts index dfa0ed5392..b719b02048 100644 --- a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts +++ b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts @@ -44,13 +44,13 @@ describe('ComponentWrapper', () => { let em: EditorModel; let dsm: DataSourceManager; let pagesDataSource: DataSource; - let flatPagesDataSource: DataSource; let wrapper: ComponentWrapper; + let firstRecord: DataRecord; const pagesData = [ - { id: 'page1', page: 'page1', title: 'Title1', content: 'content 1' }, - { id: 'page2', page: 'page2', title: 'Title2', content: 'content 2' }, - { id: 'page3', page: 'page3', title: 'Title3', content: 'content 3' }, + { id: 'page1', title: 'Title1' }, + { id: 'page2', title: 'Title2' }, + { id: 'page3', title: 'Title3' }, ]; beforeEach(() => { @@ -62,10 +62,7 @@ describe('ComponentWrapper', () => { records: [{ id: 'pages', data: pagesData }], }); - flatPagesDataSource = dsm.add({ - id: 'flatPagesDataSource', - records: pagesData, - }); + firstRecord = em.DataSources.get('pagesDataSource').getRecord('pages')!; }); afterEach(() => { @@ -104,22 +101,9 @@ describe('ComponentWrapper', () => { setResolver('pagesDataSource.pages.data'); const child = appendChildWithTitle(); - expect(child.collectionsStateMap).toEqual(wrapper.collectionsStateMap); - expect(child.collectionsStateMap).toEqual({ - [keyRootData]: pagesData[0], - }); - expect(child.get('title')).toBe(pagesData[0].title); - }); - - test('updating record propagates to children2', () => { - setResolver('flatPagesDataSource'); - const child = appendChildWithTitle(); - - expect(child.collectionsStateMap).toEqual(wrapper.collectionsStateMap); - expect(child.collectionsStateMap).toEqual({ - [keyRootData]: pagesData[0], - }); expect(child.get('title')).toBe(pagesData[0].title); + firstRecord.set('data', [{ id: 'page1', title: 'new_title' }]); + expect(child.get('title')).toBe('new_title'); }); }); }); From edd95165a9cbc8664278c83584944d11da0259b0 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Tue, 9 Sep 2025 20:28:37 +0300 Subject: [PATCH 14/33] fix component wrapper tests --- .../model/ComponentWithCollectionsState.ts | 28 +++++++++++-------- .../src/data_sources/model/DataVariable.ts | 12 ++++---- .../dom_components/model/ComponentWrapper.ts | 12 ++++---- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts index e9db448b75..e282d942ec 100644 --- a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -105,17 +105,12 @@ export default class ComponentWithCollectionsState extends Com switch (true) { case isObject(dataSource) && dataSource instanceof DataSource: { const id = dataSource.get('id')!; - return this.getDataSourceRecordsItems(id); + return this.listDataSourceVariables(id); } case isDataVariable(dataSource): { const path = dataSource.path; if (!path) return []; - const isDataSourceId = path.split('.').length === 1; - if (isDataSourceId) { - return this.getDataSourceRecordsItems(path); - } else { - return em.DataSources.getValue(path, []); - } + return this.listDataSourceVariables(path); } default: return []; @@ -132,10 +127,21 @@ export default class ComponentWithCollectionsState extends Com this.dataSourceWatcher = undefined; } - private getDataSourceRecordsItems(path: string): any[] { - const records = this.em.DataSources.getValue(path, []); - - return Object.entries(records).map(([_, value]) => value); + private listDataSourceVariables(path: string): any { + const paths = path.split('.'); + const DataSourcePath = paths[0]; + const records = this.em.DataSources.getValue(DataSourcePath, []); + if (paths.length > 1) + return { + type: DataVariableType, + path, + }; + const keys = Object.keys(records); + + return keys.map((key) => ({ + type: DataVariableType, + path: path + '.' + key, + })); } destroy(options?: ObjectAny): false | JQueryXHR { diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index e34318e014..5d314f25d2 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -5,8 +5,7 @@ import { isDataVariable } from '../utils'; import { DataCollectionStateMap, DataCollectionState, - DataCollectionStateType, - RootDataType, + DataCollectionStateType } from './data_collection/types'; export const DataVariableType = 'data-variable' as const; @@ -164,12 +163,11 @@ export default class DataVariable extends Model { const collectionItem = collectionsStateMap[collectionId]; if (!collectionItem) return defaultValue; - if (!variableType) { - if (collectionId === keyRootData) { - const rootData = collectionItem as RootDataType; - return path ? rootData[path as keyof RootDataType] : rootData; - } + if (collectionId === keyRootData) { + return { type: 'data-variable', path: `${(collectionItem as any).path!}.${path}` }; + } + if (!variableType) { em.logError(`Missing collection variable type for collection: ${collectionId}`); return defaultValue; } diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index 2063b3c540..15f3ac12a0 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -11,6 +11,7 @@ import ComponentWithCollectionsState, { DataSourceRecords, } from '../../data_sources/model/ComponentWithCollectionsState'; import { keyRootData } from '../constants'; +import { isDataResolverProps } from '../../data_sources/utils'; type ResolverCurrentItemType = string | number | undefined; @@ -144,13 +145,14 @@ export default class ComponentWrapper extends ComponentWithCollectionsState Date: Wed, 10 Sep 2025 09:08:00 +0300 Subject: [PATCH 15/33] return a copy of records for DataSource.getPath --- packages/core/src/data_sources/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/src/data_sources/index.ts b/packages/core/src/data_sources/index.ts index 5e9f6efb16..d54110a755 100644 --- a/packages/core/src/data_sources/index.ts +++ b/packages/core/src/data_sources/index.ts @@ -88,7 +88,9 @@ export default class DataSourceManager extends ItemManagerModule { const dataRecord = dr; - accR[dataRecord.id || i] = dataRecord.attributes; + const attributes = { ...dataRecord.attributes }; + delete attributes.__p; + accR[dataRecord.id || i] = attributes; return accR; }, {} as ObjectAny); From 094b0da3c3af7668daeeba3f4c048a59124c5568 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 10 Sep 2025 09:08:40 +0300 Subject: [PATCH 16/33] Move all collection listeners to component with collection state --- .../model/ComponentWithCollectionsState.ts | 30 +++++++------------ .../model/DataResolverListener.ts | 16 ++++++++-- .../src/data_sources/model/DataVariable.ts | 5 ++-- 3 files changed, 28 insertions(+), 23 deletions(-) diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts index e282d942ec..2264404919 100644 --- a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -56,13 +56,10 @@ export default class ComponentWithCollectionsState extends Com const path = this.dataSourcePath; if (!path) return; - const { em } = this; + const { em, collectionsStateMap } = this; this.dataSourceWatcher = new DataResolverListener({ em, - resolver: new DataVariable( - { type: DataVariableType, path }, - { em, collectionsStateMap: this.collectionsStateMap }, - ), + resolver: new DataVariable({ type: DataVariableType, path }, { em, collectionsStateMap }), onUpdate: () => this.onDataSourceChange(), }); } @@ -96,25 +93,20 @@ export default class ComponentWithCollectionsState extends Com } const clone = { ...items }; - delete clone['__p']; return clone; } protected listDataSourceItems(dataSource: DataSource | DataVariableProps): DataSourceRecords { - const em = this.em; - switch (true) { - case isObject(dataSource) && dataSource instanceof DataSource: { - const id = dataSource.get('id')!; - return this.listDataSourceVariables(id); - } - case isDataVariable(dataSource): { - const path = dataSource.path; - if (!path) return []; - return this.listDataSourceVariables(path); - } - default: - return []; + const path = dataSource instanceof DataSource ? dataSource.get('id')! : dataSource.path; + if (!path) return []; + let value = this.em.DataSources.getValue(path, []); + + const isDatasourceId = path.split('.').length === 1; + if (isDatasourceId) { + value = Object.entries(value).map(([_, value]) => value); } + + return value; } protected getItemKey(items: DataVariableProps[] | { [x: string]: DataVariableProps }, index: number) { diff --git a/packages/core/src/data_sources/model/DataResolverListener.ts b/packages/core/src/data_sources/model/DataResolverListener.ts index cf3d4c9c3d..6d9b7adf07 100644 --- a/packages/core/src/data_sources/model/DataResolverListener.ts +++ b/packages/core/src/data_sources/model/DataResolverListener.ts @@ -17,7 +17,7 @@ export interface DataResolverListenerProps { } interface ListenerWithCallback extends DataSourceListener { - callback: () => void; + callback: (opts?: any) => void; } export default class DataResolverListener { @@ -39,7 +39,11 @@ export default class DataResolverListener { this.onUpdate(value); }; - private createListener(obj: any, event: string, callback: () => void = this.onChange): ListenerWithCallback { + private createListener( + obj: any, + event: string, + callback: (opts?: any) => void = this.onChange, + ): ListenerWithCallback { return { obj, event, callback }; } @@ -100,6 +104,14 @@ export default class DataResolverListener { this.createListener(em, `${DataSourcesEvents.path}:${normPath}`), ); + dataListeners.push( + this.createListener(em, 'data:path', ({ path: eventPath }: { path: string }) => { + if (eventPath.startsWith(path)) { + this.onChange(); + } + }), + ); + return dataListeners; } diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index 5d314f25d2..9edddd56d6 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -5,7 +5,8 @@ import { isDataVariable } from '../utils'; import { DataCollectionStateMap, DataCollectionState, - DataCollectionStateType + DataCollectionStateType, + RootDataType, } from './data_collection/types'; export const DataVariableType = 'data-variable' as const; @@ -164,7 +165,7 @@ export default class DataVariable extends Model { if (!collectionItem) return defaultValue; if (collectionId === keyRootData) { - return { type: 'data-variable', path: `${(collectionItem as any).path!}.${path}` }; + return path ? (collectionItem as RootDataType)?.[path as keyof RootDataType] : collectionItem; } if (!variableType) { From b96dee5c63a984b6f8416e4e5a562b21226a9623 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 10 Sep 2025 09:08:52 +0300 Subject: [PATCH 17/33] fix style sync in collection items --- .../src/dom_components/model/SymbolUtils.ts | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/core/src/dom_components/model/SymbolUtils.ts b/packages/core/src/dom_components/model/SymbolUtils.ts index 4df7300f98..4066749ad7 100644 --- a/packages/core/src/dom_components/model/SymbolUtils.ts +++ b/packages/core/src/dom_components/model/SymbolUtils.ts @@ -172,17 +172,25 @@ const filterPropertiesForPropagation = (props: Record, component: C return filteredProps; }; -const shouldPropagateProperty = (props: Record, prop: string, component: Component): boolean => { - const isCollectionVariableDefinition = (() => { - if (prop === 'attributes') { - const attributes = props['attributes']; - return Object.values(attributes).some((attr: any) => !!attr?.collectionId); - } +const hasCollectionId = (obj: Record | undefined): boolean => { + if (!obj) return false; + return Object.values(obj).some((val: any) => Boolean(val?.collectionId)); +}; - return !!props[prop]?.collectionId; - })(); +const isCollectionVariableDefinition = (props: Record, prop: string): boolean => { + switch (prop) { + case 'attributes': + case 'style': + return hasCollectionId(props[prop]); + default: + return Boolean(props[prop]?.collectionId); + } +}; + +const shouldPropagateProperty = (props: Record, prop: string, component: Component): boolean => { + const isCollectionVar = isCollectionVariableDefinition(props, prop); - return !isSymbolOverride(component, prop) || isCollectionVariableDefinition; + return !isSymbolOverride(component, prop) || isCollectionVar; }; export const updateSymbolCls = (symbol: Component, opts: any = {}) => { From 2f8c198596416607a827a68fb902c47e7b0dfa06 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 10 Sep 2025 09:09:04 +0300 Subject: [PATCH 18/33] fix loop issue --- packages/core/src/dom_components/model/Component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index fe4af01580..6b2d10c2b5 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -427,9 +427,9 @@ export default class Component extends StyleableModel { } } - __onStyleChange(newStyles: StyleProps) { + __onStyleChange(newStyles: StyleProps, opts?: UpdateStyleOptions) { const { em } = this; - if (!em) return; + if (!em || opts?.noEvent) return; const styleKeys = keys(newStyles); const pros = { style: newStyles }; @@ -456,7 +456,7 @@ export default class Component extends StyleableModel { ); if (isChildOfOriginalCollections) { - component.addStyle(newStyles); + component.addStyle({ ...newStyles }, { noEvent: true }); } }); } @@ -845,7 +845,7 @@ export default class Component extends StyleableModel { } if (!opt.temporary) { - this.__onStyleChange(opts.addStyle || prop); + this.__onStyleChange(opts.addStyle || prop, opts); } return prop; From 7772c7d52d4f489a43b7b148fcee0428755b2474 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 10 Sep 2025 09:09:27 +0300 Subject: [PATCH 19/33] update data collection tests --- .../ComponentDataCollection.ts | 164 +++++++++--------- ...ComponentDataCollectionWithDataVariable.ts | 10 +- 2 files changed, 88 insertions(+), 86 deletions(-) diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts index 41169940af..a6b59a00bd 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts @@ -95,9 +95,9 @@ describe('Collection component', () => { let cmp: ComponentDataCollection; let firstChild!: Component; let firstGrandchild!: Component; - let secondChild!: Component; - let secondGrandchild!: Component; - let thirdChild!: Component; + let secondChild!: () => Component; + let secondGrandchild!: () => Component; + let thirdChild!: () => Component; const checkHtmlModelAndView = ({ cmp, innerHTML }: { cmp: Component; innerHTML: string }) => { const tagName = cmp.tagName; @@ -166,9 +166,9 @@ describe('Collection component', () => { firstChild = cmp.components().at(0).components().at(0); firstGrandchild = firstChild.components().at(0); - secondChild = cmp.components().at(1).components().at(0); - secondGrandchild = secondChild.components().at(0); - thirdChild = cmp.components().at(2).components().at(0); + secondChild = () => cmp.components().at(1).components().at(0); + secondGrandchild = () => secondChild().components().at(0); + thirdChild = () => cmp.components().at(2).components().at(0); }); test('Evaluating to static value', () => { @@ -176,9 +176,9 @@ describe('Collection component', () => { expect(firstChild.get('custom_property')).toBe('user1'); expect(firstGrandchild.get('name')).toBe('user1'); - expect(secondChild.get('name')).toBe('user2'); - expect(secondChild.get('custom_property')).toBe('user2'); - expect(secondGrandchild.get('name')).toBe('user2'); + expect(secondChild().get('name')).toBe('user2'); + expect(secondChild().get('custom_property')).toBe('user2'); + expect(secondGrandchild().get('name')).toBe('user2'); checkRecordsWithInnerCmp(); }); @@ -189,9 +189,9 @@ describe('Collection component', () => { expect(firstChild.get('custom_property')).toBe('new_user1_value'); expect(firstGrandchild.get('name')).toBe('new_user1_value'); - expect(secondChild.get('name')).toBe('user2'); - expect(secondChild.get('custom_property')).toBe('user2'); - expect(secondGrandchild.get('name')).toBe('user2'); + expect(secondChild().get('name')).toBe('user2'); + expect(secondChild().get('custom_property')).toBe('user2'); + expect(secondGrandchild().get('name')).toBe('user2'); const firstName = 'Name1-up'; firstRecord.set({ firstName }); @@ -230,8 +230,8 @@ describe('Collection component', () => { expect(newGrandchild.get('name')).toBe('user4'); expect(firstChild.get('name')).toBe('user1'); - expect(secondChild.get('name')).toBe('user2'); - expect(thirdChild.get('name')).toBe('user3'); + expect(secondChild().get('name')).toBe('user2'); + expect(thirdChild().get('name')).toBe('user3'); checkRecordsWithInnerCmp(); }); @@ -239,22 +239,22 @@ describe('Collection component', () => { test('Updating the value to a static value', async () => { firstChild.set('name', 'new_content_value'); expect(firstChild.get('name')).toBe('new_content_value'); - expect(secondChild.get('name')).toBe('new_content_value'); + expect(secondChild().get('name')).toBe('new_content_value'); firstRecord.set('user', 'wrong_value'); expect(firstChild.get('name')).toBe('new_content_value'); - expect(secondChild.get('name')).toBe('new_content_value'); + expect(secondChild().get('name')).toBe('new_content_value'); firstGrandchild.set('name', 'new_content_value'); expect(firstGrandchild.get('name')).toBe('new_content_value'); - expect(secondGrandchild.get('name')).toBe('new_content_value'); + expect(secondGrandchild().get('name')).toBe('new_content_value'); firstRecord.set('user', 'wrong_value'); expect(firstGrandchild.get('name')).toBe('new_content_value'); - expect(secondGrandchild.get('name')).toBe('new_content_value'); + expect(secondGrandchild().get('name')).toBe('new_content_value'); }); - test('Updating the value to a different collection variable', async () => { + test('Updating the value to a different collection variable', (done) => { firstChild.set('name', { type: DataVariableType, variableType: DataCollectionStateType.currentItem, @@ -262,7 +262,7 @@ describe('Collection component', () => { path: 'age', }); expect(firstChild.get('name')).toBe('12'); - expect(secondChild.get('name')).toBe('14'); + expect(secondChild().get('name')).toBe('14'); firstRecord.set('age', 'new_value_12'); secondRecord.set('age', 'new_value_14'); @@ -271,7 +271,7 @@ describe('Collection component', () => { secondRecord.set('user', 'wrong_value'); expect(firstChild.get('name')).toBe('new_value_12'); - expect(secondChild.get('name')).toBe('new_value_14'); + expect(secondChild().get('name')).toBe('new_value_14'); firstGrandchild.set('name', { type: DataVariableType, @@ -280,13 +280,13 @@ describe('Collection component', () => { path: 'age', }); expect(firstGrandchild.get('name')).toBe('new_value_12'); - expect(secondGrandchild.get('name')).toBe('new_value_14'); + expect(secondGrandchild().get('name')).toBe('new_value_14'); firstRecord.set('age', 'most_new_value_12'); secondRecord.set('age', 'most_new_value_14'); expect(firstGrandchild.get('name')).toBe('most_new_value_12'); - expect(secondGrandchild.get('name')).toBe('most_new_value_14'); + expect(secondGrandchild().get('name')).toBe('most_new_value_14'); }); test('Updating the value to a different dynamic variable', async () => { @@ -295,13 +295,13 @@ describe('Collection component', () => { path: 'my_data_source_id.user2.user', }); expect(firstChild.get('name')).toBe('user2'); - expect(secondChild.get('name')).toBe('user2'); - expect(thirdChild.get('name')).toBe('user2'); + expect(secondChild().get('name')).toBe('user2'); + expect(thirdChild().get('name')).toBe('user2'); secondRecord.set('user', 'new_value'); expect(firstChild.get('name')).toBe('new_value'); - expect(secondChild.get('name')).toBe('new_value'); - expect(thirdChild.get('name')).toBe('new_value'); + expect(secondChild().get('name')).toBe('new_value'); + expect(thirdChild().get('name')).toBe('new_value'); // @ts-ignore firstGrandchild.set('name', { @@ -309,12 +309,12 @@ describe('Collection component', () => { path: 'my_data_source_id.user2.user', }); expect(firstGrandchild.get('name')).toBe('new_value'); - expect(secondGrandchild.get('name')).toBe('new_value'); + expect(secondGrandchild().get('name')).toBe('new_value'); secondRecord.set('user', 'most_new_value'); expect(firstGrandchild.get('name')).toBe('most_new_value'); - expect(secondGrandchild.get('name')).toBe('most_new_value'); + expect(secondGrandchild().get('name')).toBe('most_new_value'); }); }); @@ -322,9 +322,9 @@ describe('Collection component', () => { let cmp: Component; let firstChild!: Component; let firstGrandchild!: Component; - let secondChild!: Component; - let secondGrandchild!: Component; - let thirdChild!: Component; + let secondChild!: () => Component; + let secondGrandchild!: () => Component; + let thirdChild!: () => Component; beforeEach(() => { const cmpDef = { @@ -368,9 +368,9 @@ describe('Collection component', () => { firstChild = cmp.components().at(0).components().at(0); firstGrandchild = firstChild.components().at(0); - secondChild = cmp.components().at(1).components().at(0); - secondGrandchild = secondChild.components().at(0); - thirdChild = cmp.components().at(2).components().at(0); + secondChild = () => cmp.components().at(1).components().at(0); + secondGrandchild = () => secondChild().components().at(0); + thirdChild = () => cmp.components().at(2).components().at(0); }); test('Evaluating to static value', () => { @@ -379,10 +379,10 @@ describe('Collection component', () => { expect(firstGrandchild.getAttributes()['name']).toBe('user1'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('user1'); - expect(secondChild.getAttributes()['name']).toBe('user2'); - expect(secondChild.getEl()?.getAttribute('name')).toBe('user2'); - expect(secondGrandchild.getAttributes()['name']).toBe('user2'); - expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('user2'); + expect(secondChild().getAttributes()['name']).toBe('user2'); + expect(secondChild().getEl()?.getAttribute('name')).toBe('user2'); + expect(secondGrandchild().getAttributes()['name']).toBe('user2'); + expect(secondGrandchild().getEl()?.getAttribute('name')).toBe('user2'); }); test('Watching Records', async () => { @@ -392,30 +392,32 @@ describe('Collection component', () => { expect(firstGrandchild.getAttributes()['name']).toBe('new_user1_value'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('new_user1_value'); - expect(secondChild.getAttributes()['name']).toBe('user2'); - expect(secondGrandchild.getAttributes()['name']).toBe('user2'); + expect(secondChild().getAttributes()['name']).toBe('user2'); + expect(secondGrandchild().getAttributes()['name']).toBe('user2'); }); test('Updating the value to a static value', async () => { firstChild.setAttributes({ name: 'new_content_value' }); expect(firstChild.getAttributes()['name']).toBe('new_content_value'); expect(firstChild.getEl()?.getAttribute('name')).toBe('new_content_value'); - expect(secondChild.getAttributes()['name']).toBe('new_content_value'); - expect(secondChild.getEl()?.getAttribute('name')).toBe('new_content_value'); + expect(secondChild().getAttributes()['name']).toBe('new_content_value'); + expect(secondChild().getEl()?.getAttribute('name')).toBe('new_content_value'); firstRecord.set('user', 'wrong_value'); + secondChild = () => cmp.components().at(1).components().at(0); + secondGrandchild = () => secondChild().components().at(0); expect(firstChild.getAttributes()['name']).toBe('new_content_value'); expect(firstChild.getEl()?.getAttribute('name')).toBe('new_content_value'); - expect(secondChild.getAttributes()['name']).toBe('new_content_value'); - expect(secondChild.getEl()?.getAttribute('name')).toBe('new_content_value'); + expect(secondChild().getAttributes()['name']).toBe('new_content_value'); + expect(secondChild().getEl()?.getAttribute('name')).toBe('new_content_value'); firstGrandchild.setAttributes({ name: 'new_content_value' }); expect(firstGrandchild.getAttributes()['name']).toBe('new_content_value'); - expect(secondGrandchild.getAttributes()['name']).toBe('new_content_value'); + expect(secondGrandchild().getAttributes()['name']).toBe('new_content_value'); firstRecord.set('user', 'wrong_value'); expect(firstGrandchild.getAttributes()['name']).toBe('new_content_value'); - expect(secondGrandchild.getAttributes()['name']).toBe('new_content_value'); + expect(secondGrandchild().getAttributes()['name']).toBe('new_content_value'); }); test('Updating the value to a diffirent collection variable', async () => { @@ -430,8 +432,8 @@ describe('Collection component', () => { }); expect(firstChild.getAttributes()['name']).toBe('12'); expect(firstChild.getEl()?.getAttribute('name')).toBe('12'); - expect(secondChild.getAttributes()['name']).toBe('14'); - expect(secondChild.getEl()?.getAttribute('name')).toBe('14'); + expect(secondChild().getAttributes()['name']).toBe('14'); + expect(secondChild().getEl()?.getAttribute('name')).toBe('14'); firstRecord.set('age', 'new_value_12'); secondRecord.set('age', 'new_value_14'); @@ -441,8 +443,8 @@ describe('Collection component', () => { expect(firstChild.getAttributes()['name']).toBe('new_value_12'); expect(firstChild.getEl()?.getAttribute('name')).toBe('new_value_12'); - expect(secondChild.getAttributes()['name']).toBe('new_value_14'); - expect(secondChild.getEl()?.getAttribute('name')).toBe('new_value_14'); + expect(secondChild().getAttributes()['name']).toBe('new_value_14'); + expect(secondChild().getEl()?.getAttribute('name')).toBe('new_value_14'); firstGrandchild.setAttributes({ name: { @@ -455,14 +457,14 @@ describe('Collection component', () => { }); expect(firstGrandchild.getAttributes()['name']).toBe('new_value_12'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('new_value_12'); - expect(secondGrandchild.getAttributes()['name']).toBe('new_value_14'); - expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('new_value_14'); + expect(secondGrandchild().getAttributes()['name']).toBe('new_value_14'); + expect(secondGrandchild().getEl()?.getAttribute('name')).toBe('new_value_14'); firstRecord.set('age', 'most_new_value_12'); secondRecord.set('age', 'most_new_value_14'); expect(firstGrandchild.getAttributes()['name']).toBe('most_new_value_12'); - expect(secondGrandchild.getAttributes()['name']).toBe('most_new_value_14'); + expect(secondGrandchild().getAttributes()['name']).toBe('most_new_value_14'); }); test('Updating the value to a different dynamic variable', async () => { @@ -475,16 +477,16 @@ describe('Collection component', () => { }); expect(firstChild.getAttributes()['name']).toBe('user2'); expect(firstChild.getEl()?.getAttribute('name')).toBe('user2'); - expect(secondChild.getAttributes()['name']).toBe('user2'); - expect(secondChild.getEl()?.getAttribute('name')).toBe('user2'); - expect(thirdChild.getAttributes()['name']).toBe('user2'); + expect(secondChild().getAttributes()['name']).toBe('user2'); + expect(secondChild().getEl()?.getAttribute('name')).toBe('user2'); + expect(thirdChild().getAttributes()['name']).toBe('user2'); secondRecord.set('user', 'new_value'); expect(firstChild.getAttributes()['name']).toBe('new_value'); expect(firstChild.getEl()?.getAttribute('name')).toBe('new_value'); - expect(secondChild.getAttributes()['name']).toBe('new_value'); - expect(secondChild.getEl()?.getAttribute('name')).toBe('new_value'); - expect(thirdChild.getAttributes()['name']).toBe('new_value'); + expect(secondChild().getAttributes()['name']).toBe('new_value'); + expect(secondChild().getEl()?.getAttribute('name')).toBe('new_value'); + expect(thirdChild().getAttributes()['name']).toBe('new_value'); firstGrandchild.setAttributes({ name: { @@ -495,15 +497,15 @@ describe('Collection component', () => { }); expect(firstGrandchild.getAttributes()['name']).toBe('new_value'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('new_value'); - expect(secondGrandchild.getAttributes()['name']).toBe('new_value'); - expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('new_value'); + expect(secondGrandchild().getAttributes()['name']).toBe('new_value'); + expect(secondGrandchild().getEl()?.getAttribute('name')).toBe('new_value'); secondRecord.set('user', 'most_new_value'); expect(firstGrandchild.getAttributes()['name']).toBe('most_new_value'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('most_new_value'); - expect(secondGrandchild.getAttributes()['name']).toBe('most_new_value'); - expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('most_new_value'); + expect(secondGrandchild().getAttributes()['name']).toBe('most_new_value'); + expect(secondGrandchild().getEl()?.getAttribute('name')).toBe('most_new_value'); }); }); @@ -549,24 +551,24 @@ describe('Collection component', () => { expect(cmp.getItemsCount()).toBe(3); const firstChild = cmp.components().at(0).components().at(0); - const secondChild = cmp.components().at(1).components().at(0); + const secondChild = () => cmp.components().at(1).components().at(0); expect(firstChild.getAttributes()['attribute_trait']).toBe('user1'); expect(firstChild.getEl()?.getAttribute('attribute_trait')).toBe('user1'); expect(firstChild.get('property_trait')).toBe('user1'); - expect(secondChild.getAttributes()['attribute_trait']).toBe('user2'); - expect(secondChild.getEl()?.getAttribute('attribute_trait')).toBe('user2'); - expect(secondChild.get('property_trait')).toBe('user2'); + expect(secondChild().getAttributes()['attribute_trait']).toBe('user2'); + expect(secondChild().getEl()?.getAttribute('attribute_trait')).toBe('user2'); + expect(secondChild().get('property_trait')).toBe('user2'); firstRecord.set('user', 'new_user1_value'); expect(firstChild.getAttributes()['attribute_trait']).toBe('new_user1_value'); expect(firstChild.getEl()?.getAttribute('attribute_trait')).toBe('new_user1_value'); expect(firstChild.get('property_trait')).toBe('new_user1_value'); - expect(secondChild.getAttributes()['attribute_trait']).toBe('user2'); - expect(secondChild.getEl()?.getAttribute('attribute_trait')).toBe('user2'); - expect(secondChild.get('property_trait')).toBe('user2'); + expect(secondChild().getAttributes()['attribute_trait']).toBe('user2'); + expect(secondChild().getEl()?.getAttribute('attribute_trait')).toBe('user2'); + expect(secondChild().get('property_trait')).toBe('user2'); }); }); @@ -852,18 +854,18 @@ describe('Collection component', () => { const component = components.models[0] as ComponentDataCollection; const firstChild = component.components().at(0).components().at(0); const firstGrandchild = firstChild.components().at(0); - const secondChild = component.components().at(1).components().at(0); - const secondGrandchild = secondChild.components().at(0); + const secondChild = () => component.components().at(1).components().at(0); + const secondGrandchild = () => secondChild().components().at(0); expect(firstChild.get('name')).toBe('user1'); expect(firstChild.getAttributes()['name']).toBe('user1'); expect(firstGrandchild.get('name')).toBe('user1'); expect(firstGrandchild.getAttributes()['name']).toBe('user1'); - expect(secondChild.get('name')).toBe('user2'); - expect(secondChild.getAttributes()['name']).toBe('user2'); - expect(secondGrandchild.get('name')).toBe('user2'); - expect(secondGrandchild.getAttributes()['name']).toBe('user2'); + expect(secondChild().get('name')).toBe('user2'); + expect(secondChild().getAttributes()['name']).toBe('user2'); + expect(secondGrandchild().get('name')).toBe('user2'); + expect(secondGrandchild().getAttributes()['name']).toBe('user2'); firstRecord.set('user', 'new_user1_value'); expect(firstChild.get('name')).toBe('new_user1_value'); @@ -871,10 +873,10 @@ describe('Collection component', () => { expect(firstGrandchild.get('name')).toBe('new_user1_value'); expect(firstGrandchild.getAttributes()['name']).toBe('new_user1_value'); - expect(secondChild.get('name')).toBe('user2'); - expect(secondChild.getAttributes()['name']).toBe('user2'); - expect(secondGrandchild.get('name')).toBe('user2'); - expect(secondGrandchild.getAttributes()['name']).toBe('user2'); + expect(secondChild().get('name')).toBe('user2'); + expect(secondChild().getAttributes()['name']).toBe('user2'); + expect(secondGrandchild().get('name')).toBe('user2'); + expect(secondGrandchild().getAttributes()['name']).toBe('user2'); }); }); diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts index 764667c763..fd8bcccf8e 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts @@ -255,19 +255,19 @@ describe('Collection variable components', () => { 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); + const secondChild = () => component.components().at(1); + const secondGrandchild = () => secondChild().components().at(0); expect(firstGrandchild.getInnerHTML()).toBe('user1'); - expect(secondGrandchild.getInnerHTML()).toBe('user2'); + expect(secondGrandchild().getInnerHTML()).toBe('user2'); firstRecord.set('user', 'new_user1_value'); expect(firstGrandchild.getInnerHTML()).toBe('new_user1_value'); - expect(secondGrandchild.getInnerHTML()).toBe('user2'); + 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'); + expect(secondGrandchild().getInnerHTML()).toBe('new_user2_value'); }); }); }); From eb9b4ff40b5e3d8bb98c800d8e9a0fee544127b3 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 10 Sep 2025 09:09:38 +0300 Subject: [PATCH 20/33] cleanup --- packages/core/src/dom_components/model/ModelResolverWatcher.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/core/src/dom_components/model/ModelResolverWatcher.ts b/packages/core/src/dom_components/model/ModelResolverWatcher.ts index 82569aa920..0834721eea 100644 --- a/packages/core/src/dom_components/model/ModelResolverWatcher.ts +++ b/packages/core/src/dom_components/model/ModelResolverWatcher.ts @@ -56,9 +56,6 @@ export class ModelResolverWatcher { onCollectionsStateMapUpdate() { const resolvesFromCollections = this.getValuesResolvingFromCollections(); if (!resolvesFromCollections.length) return; - resolvesFromCollections.forEach((key) => - this.resolverListeners[key].resolver.updateCollectionsStateMap(this.collectionsStateMap), - ); const evaluatedValues = this.addDataValues( this.getValuesOrResolver(Object.fromEntries(resolvesFromCollections.map((key) => [key, '']))), From 4499b0640dab1bb8b5ff81915f5362e5cb1ff575 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 10 Sep 2025 09:09:57 +0300 Subject: [PATCH 21/33] update collection statemap on wrapper change --- .../core/src/dom_components/model/ComponentWrapper.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index 15f3ac12a0..bc5a6cebc5 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -7,9 +7,7 @@ import Components from './Components'; import DataResolverListener from '../../data_sources/model/DataResolverListener'; import { DataVariableProps } from '../../data_sources/model/DataVariable'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; -import ComponentWithCollectionsState, { - DataSourceRecords, -} from '../../data_sources/model/ComponentWithCollectionsState'; +import ComponentWithCollectionsState from '../../data_sources/model/ComponentWithCollectionsState'; import { keyRootData } from '../constants'; import { isDataResolverProps } from '../../data_sources/utils'; @@ -51,7 +49,7 @@ export default class ComponentWrapper extends ComponentWithCollectionsState { this.onCollectionsStateMapUpdate(this.getCollectionsStateMap()); From 6a71c87288b0f77b93a30f569fd127eb54d2ce0d Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 10 Sep 2025 09:23:02 +0300 Subject: [PATCH 22/33] Add object test data for wrapper data resolver --- .../dom_components/model/ComponentWrapper.ts | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts index b719b02048..91938b8e2b 100644 --- a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts +++ b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts @@ -47,11 +47,12 @@ describe('ComponentWrapper', () => { let wrapper: ComponentWrapper; let firstRecord: DataRecord; - const pagesData = [ - { id: 'page1', title: 'Title1' }, - { id: 'page2', title: 'Title2' }, - { id: 'page3', title: 'Title3' }, - ]; + const firstPageData = { id: 'page1', title: 'Title1' }; + const pagesData = [firstPageData, { id: 'page2', title: 'Title2' }, { id: 'page3', title: 'Title3' }]; + const objectData = { + page1: { title: 'page1' }, + page2: { title: 'page2' }, + }; beforeEach(() => { ({ em, dsm } = setupTestEditor()); @@ -59,7 +60,13 @@ describe('ComponentWrapper', () => { pagesDataSource = dsm.add({ id: 'pagesDataSource', - records: [{ id: 'pages', data: pagesData }], + records: [ + { id: 'pages', data: pagesData }, + { + id: 'objectData', + data: objectData, + }, + ], }); firstRecord = em.DataSources.get('pagesDataSource').getRecord('pages')!; @@ -74,36 +81,57 @@ describe('ComponentWrapper', () => { path, }); - const setResolver = (path: string) => { - wrapper.setDataResolver(createDataResolver(path)); - wrapper.resolverCurrentItem = 0; - }; - - const appendChildWithTitle = () => + const appendChildWithTitle = (path: string = 'title') => wrapper.append({ type: 'default', title: { type: 'data-variable', collectionId: keyRootData, - path: 'title', + path, }, })[0]; test('sets dataResolver and updates wrapper.page/head collectionsStateMap', () => { - setResolver('pagesDataSource.pages.data'); + wrapper.setDataResolver(createDataResolver('pagesDataSource.pages.data')); + wrapper.resolverCurrentItem = 0; const stateMap = wrapper.collectionsStateMap; expect(stateMap).toHaveProperty(keyRootData); expect(wrapper.head.collectionsStateMap).toEqual(stateMap); + expect(wrapper.head.collectionsStateMap).toEqual({ [keyRootData]: firstPageData }); }); test('children reflect resolved value from dataResolver', () => { - setResolver('pagesDataSource.pages.data'); + wrapper.setDataResolver(createDataResolver('pagesDataSource.pages.data')); + wrapper.resolverCurrentItem = 0; const child = appendChildWithTitle(); + expect(child.collectionsStateMap).toEqual({ [keyRootData]: firstPageData }); expect(child.get('title')).toBe(pagesData[0].title); + firstRecord.set('data', [{ id: 'page1', title: 'new_title' }]); expect(child.get('title')).toBe('new_title'); }); + + test('children update collectionStateMap on wrapper.setDataResolver', () => { + const child = appendChildWithTitle(); + wrapper.setDataResolver(createDataResolver('pagesDataSource.pages.data')); + wrapper.resolverCurrentItem = 0; + + expect(child.collectionsStateMap).toEqual({ [keyRootData]: firstPageData }); + expect(child.get('title')).toBe(pagesData[0].title); + + firstRecord.set('data', [{ id: 'page1', title: 'new_title' }]); + expect(child.get('title')).toBe('new_title'); + }); + + test('wrapper should handle objects as collection state ', () => { + wrapper.setDataResolver(createDataResolver('pagesDataSource.objectData.data')); + wrapper.resolverCurrentItem = 'page1'; + const child = appendChildWithTitle('title'); + + expect(child.collectionsStateMap).toEqual({ [keyRootData]: objectData.page1 }); + expect(child.get('title')).toBe(objectData.page1.title); + }); }); }); From fb84c3c7b2e326c223e7cb576d847e4664351717 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Wed, 10 Sep 2025 09:39:01 +0300 Subject: [PATCH 23/33] cleanup --- .../model/ComponentWithCollectionsState.ts | 21 +------------ .../dom_components/model/ComponentWrapper.ts | 30 +++++++++---------- 2 files changed, 15 insertions(+), 36 deletions(-) diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts index 2264404919..e593f9f8f8 100644 --- a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -4,8 +4,6 @@ import DataVariable, { DataVariableProps, DataVariableType } from '../../data_so import Components from '../../dom_components/model/Components'; import Component from '../../dom_components/model/Component'; import { ObjectAny } from '../../common'; -import { isDataVariable } from '../utils'; -import { isObject } from '../../utils/mixins'; import DataSource from './DataSource'; import { isArray } from 'underscore'; @@ -84,7 +82,7 @@ export default class ComponentWithCollectionsState extends Com this.onCollectionsStateMapUpdate(this.collectionsStateMap); } - protected getDataSourceItems() { + protected getDataSourceItems(): DataSourceRecords { const dataSourceProps = this.dataSourceProps; if (!dataSourceProps) return []; const items = this.listDataSourceItems(dataSourceProps); @@ -119,23 +117,6 @@ export default class ComponentWithCollectionsState extends Com this.dataSourceWatcher = undefined; } - private listDataSourceVariables(path: string): any { - const paths = path.split('.'); - const DataSourcePath = paths[0]; - const records = this.em.DataSources.getValue(DataSourcePath, []); - if (paths.length > 1) - return { - type: DataVariableType, - path, - }; - const keys = Object.keys(records); - - return keys.map((key) => ({ - type: DataVariableType, - path: path + '.' + key, - })); - } - destroy(options?: ObjectAny): false | JQueryXHR { this.removePropsListeners(); return super.destroy(options); diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index bc5a6cebc5..e7d00a82fc 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -7,15 +7,16 @@ import Components from './Components'; import DataResolverListener from '../../data_sources/model/DataResolverListener'; import { DataVariableProps } from '../../data_sources/model/DataVariable'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; -import ComponentWithCollectionsState from '../../data_sources/model/ComponentWithCollectionsState'; +import ComponentWithCollectionsState, { + DataSourceRecords, +} from '../../data_sources/model/ComponentWithCollectionsState'; import { keyRootData } from '../constants'; -import { isDataResolverProps } from '../../data_sources/utils'; -type ResolverCurrentItemType = string | number | undefined; +type ResolverCurrentItemType = string | number; export default class ComponentWrapper extends ComponentWithCollectionsState { dataSourceWatcher?: DataResolverListener; - private _resolverCurrentItem: ResolverCurrentItemType; + private _resolverCurrentItem?: ResolverCurrentItemType; get defaults() { return { @@ -120,7 +121,7 @@ export default class ComponentWrapper extends ComponentWithCollectionsState Date: Wed, 10 Sep 2025 09:48:00 +0300 Subject: [PATCH 24/33] up unit test --- .../model/data_collection/ComponentDataCollection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts index a6b59a00bd..43c6488fbc 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts @@ -254,7 +254,7 @@ describe('Collection component', () => { expect(secondGrandchild().get('name')).toBe('new_content_value'); }); - test('Updating the value to a different collection variable', (done) => { + test('Updating the value to a different collection variable', () => { firstChild.set('name', { type: DataVariableType, variableType: DataCollectionStateType.currentItem, From aef82694bd69b5c8207c48357547b85ffe598b9b Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 29 Sep 2025 10:29:49 +0300 Subject: [PATCH 25/33] remove duplicated code --- .../src/data_sources/model/ComponentWithCollectionsState.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts index e593f9f8f8..effaecb23d 100644 --- a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -37,11 +37,6 @@ export default class ComponentWithCollectionsState extends Com this.onCollectionsStateMapUpdate(prev); } - syncComponentsCollectionState() { - super.syncComponentsCollectionState(); - this.components().forEach((cmp) => cmp.syncComponentsCollectionState?.()); - } - setDataResolver(dataResolver: DataResolverType | undefined) { return this.set('dataResolver', dataResolver); } From f26a3654949a0e2bb2d00e35d8548c5c6666c933 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 29 Sep 2025 10:30:01 +0300 Subject: [PATCH 26/33] cleanup event path --- packages/core/src/data_sources/model/DataResolverListener.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/data_sources/model/DataResolverListener.ts b/packages/core/src/data_sources/model/DataResolverListener.ts index 6d9b7adf07..bf1c805422 100644 --- a/packages/core/src/data_sources/model/DataResolverListener.ts +++ b/packages/core/src/data_sources/model/DataResolverListener.ts @@ -105,7 +105,7 @@ export default class DataResolverListener { ); dataListeners.push( - this.createListener(em, 'data:path', ({ path: eventPath }: { path: string }) => { + this.createListener(em, DataSourcesEvents.path, ({ path: eventPath }: { path: string }) => { if (eventPath.startsWith(path)) { this.onChange(); } From d2c25794479afba42e9cae26236f198ac5a76ffc Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 29 Sep 2025 10:30:18 +0300 Subject: [PATCH 27/33] update test data to better names --- .../src/data_sources/model/DataVariable.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index 9edddd56d6..b8dbb1258c 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -146,26 +146,25 @@ export default class DataVariable extends Model { } static resolveCollectionVariable( - { - collectionId = '', - variableType, - path, - defaultValue = '', - }: { + params: { collectionId?: string; variableType?: DataCollectionStateType; path?: string; defaultValue?: string; }, - { em, collectionsStateMap }: DataVariableOptions, + ctx: DataVariableOptions, ) { + const { collectionId = '', variableType, path, defaultValue = '' } = params; + const { em, collectionsStateMap } = ctx; + if (!collectionsStateMap) return defaultValue; const collectionItem = collectionsStateMap[collectionId]; if (!collectionItem) return defaultValue; if (collectionId === keyRootData) { - return path ? (collectionItem as RootDataType)?.[path as keyof RootDataType] : collectionItem; + const root = collectionItem as RootDataType; + return path ? root?.[path as keyof RootDataType] : root; } if (!variableType) { @@ -174,10 +173,11 @@ export default class DataVariable extends Model { } if (variableType === 'currentItem') { - return DataVariable.resolveCurrentItem(collectionItem, path, collectionId, em); + return DataVariable.resolveCurrentItem(collectionItem as DataCollectionState, path, collectionId, em); } - return collectionItem[variableType] ?? defaultValue; + const state = collectionItem as DataCollectionState; + return state[variableType] ?? defaultValue; } private static resolveCurrentItem( From 56f46fd293e2bf9eb73f25c327ead0856e14ea77 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 29 Sep 2025 10:30:57 +0300 Subject: [PATCH 28/33] improve component data collection performance --- .../ComponentDataCollection.ts | 29 ++-- .../ComponentDataCollection.ts | 164 +++++++++--------- 2 files changed, 99 insertions(+), 94 deletions(-) diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index 600301a530..902b9ad459 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -1,4 +1,4 @@ -import { isArray } from 'underscore'; +import { isArray, size } from 'underscore'; import { ObjectAny } from '../../../common'; import Component, { keySymbol } from '../../../dom_components/model/Component'; import { ComponentAddType, ComponentDefinitionDefined, ComponentOptions } from '../../../dom_components/model/types'; @@ -168,14 +168,24 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta } private rebuildChildrenFromCollection() { - this.components().reset(this.getCollectionItems(), updateFromWatcher as any); + const items = this.getDataSourceItems(); + const itemsCount = size(items); + + if (itemsCount === this.components().length) { + this.onCollectionsStateMapUpdate(this.collectionsStateMap); + return; + } + + const collectionItems = this.getCollectionItems(items as any); + this.components().reset(collectionItems, updateFromWatcher as any); } - private getCollectionItems() { + private getCollectionItems(items?: any[]) { const firstChild = this.ensureFirstChild(); const displayStyle = firstChild.getStyle()['display']; const isDisplayNoneOrMissing = !displayStyle || displayStyle === 'none'; const resolvedDisplay = isDisplayNoneOrMissing ? '' : displayStyle; + // TODO: Move to component view firstChild.addStyle({ display: 'none' }, AvoidStoreOptions); const components: Component[] = [firstChild]; @@ -186,34 +196,31 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta } const collectionId = this.collectionId; - const items = this.getDataSourceItems(); - const { startIndex, endIndex } = this.resolveCollectionConfig(items); + const dataItems = items ?? this.getDataSourceItems(); + const { startIndex, endIndex } = this.resolveCollectionConfig(dataItems); const isDuplicatedId = this.hasDuplicateCollectionId(); if (isDuplicatedId) { this.em.logError( `The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`, ); - return components; } for (let index = startIndex; index <= endIndex; index++) { const isFirstItem = index === startIndex; - const key = this.getItemKey(items, index); - const collectionsStateMap = this.getCollectionsStateMapForItem(items, key); + const key = this.getItemKey(dataItems, index); + const collectionsStateMap = this.getCollectionsStateMapForItem(dataItems, key); if (isFirstItem) { getSymbolInstances(firstChild)?.forEach((cmp) => detachSymbolInstance(cmp)); - this.setCollectionStateMapAndPropagate(firstChild, collectionsStateMap); // TODO: Move to component view firstChild.addStyle({ display: resolvedDisplay }, AvoidStoreOptions); - continue; } - const instance = firstChild!.clone({ symbol: true, symbolInv: true }); + const instance = firstChild.clone({ symbol: true, symbolInv: true }); instance.set({ locked: true, layerable: false }, AvoidStoreOptions); this.setCollectionStateMapAndPropagate(instance, collectionsStateMap); components.push(instance); diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts index 43c6488fbc..41169940af 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts @@ -95,9 +95,9 @@ describe('Collection component', () => { let cmp: ComponentDataCollection; let firstChild!: Component; let firstGrandchild!: Component; - let secondChild!: () => Component; - let secondGrandchild!: () => Component; - let thirdChild!: () => Component; + let secondChild!: Component; + let secondGrandchild!: Component; + let thirdChild!: Component; const checkHtmlModelAndView = ({ cmp, innerHTML }: { cmp: Component; innerHTML: string }) => { const tagName = cmp.tagName; @@ -166,9 +166,9 @@ describe('Collection component', () => { firstChild = cmp.components().at(0).components().at(0); firstGrandchild = firstChild.components().at(0); - secondChild = () => cmp.components().at(1).components().at(0); - secondGrandchild = () => secondChild().components().at(0); - thirdChild = () => cmp.components().at(2).components().at(0); + secondChild = cmp.components().at(1).components().at(0); + secondGrandchild = secondChild.components().at(0); + thirdChild = cmp.components().at(2).components().at(0); }); test('Evaluating to static value', () => { @@ -176,9 +176,9 @@ describe('Collection component', () => { expect(firstChild.get('custom_property')).toBe('user1'); expect(firstGrandchild.get('name')).toBe('user1'); - expect(secondChild().get('name')).toBe('user2'); - expect(secondChild().get('custom_property')).toBe('user2'); - expect(secondGrandchild().get('name')).toBe('user2'); + expect(secondChild.get('name')).toBe('user2'); + expect(secondChild.get('custom_property')).toBe('user2'); + expect(secondGrandchild.get('name')).toBe('user2'); checkRecordsWithInnerCmp(); }); @@ -189,9 +189,9 @@ describe('Collection component', () => { expect(firstChild.get('custom_property')).toBe('new_user1_value'); expect(firstGrandchild.get('name')).toBe('new_user1_value'); - expect(secondChild().get('name')).toBe('user2'); - expect(secondChild().get('custom_property')).toBe('user2'); - expect(secondGrandchild().get('name')).toBe('user2'); + expect(secondChild.get('name')).toBe('user2'); + expect(secondChild.get('custom_property')).toBe('user2'); + expect(secondGrandchild.get('name')).toBe('user2'); const firstName = 'Name1-up'; firstRecord.set({ firstName }); @@ -230,8 +230,8 @@ describe('Collection component', () => { expect(newGrandchild.get('name')).toBe('user4'); expect(firstChild.get('name')).toBe('user1'); - expect(secondChild().get('name')).toBe('user2'); - expect(thirdChild().get('name')).toBe('user3'); + expect(secondChild.get('name')).toBe('user2'); + expect(thirdChild.get('name')).toBe('user3'); checkRecordsWithInnerCmp(); }); @@ -239,22 +239,22 @@ describe('Collection component', () => { test('Updating the value to a static value', async () => { firstChild.set('name', 'new_content_value'); expect(firstChild.get('name')).toBe('new_content_value'); - expect(secondChild().get('name')).toBe('new_content_value'); + expect(secondChild.get('name')).toBe('new_content_value'); firstRecord.set('user', 'wrong_value'); expect(firstChild.get('name')).toBe('new_content_value'); - expect(secondChild().get('name')).toBe('new_content_value'); + expect(secondChild.get('name')).toBe('new_content_value'); firstGrandchild.set('name', 'new_content_value'); expect(firstGrandchild.get('name')).toBe('new_content_value'); - expect(secondGrandchild().get('name')).toBe('new_content_value'); + expect(secondGrandchild.get('name')).toBe('new_content_value'); firstRecord.set('user', 'wrong_value'); expect(firstGrandchild.get('name')).toBe('new_content_value'); - expect(secondGrandchild().get('name')).toBe('new_content_value'); + expect(secondGrandchild.get('name')).toBe('new_content_value'); }); - test('Updating the value to a different collection variable', () => { + test('Updating the value to a different collection variable', async () => { firstChild.set('name', { type: DataVariableType, variableType: DataCollectionStateType.currentItem, @@ -262,7 +262,7 @@ describe('Collection component', () => { path: 'age', }); expect(firstChild.get('name')).toBe('12'); - expect(secondChild().get('name')).toBe('14'); + expect(secondChild.get('name')).toBe('14'); firstRecord.set('age', 'new_value_12'); secondRecord.set('age', 'new_value_14'); @@ -271,7 +271,7 @@ describe('Collection component', () => { secondRecord.set('user', 'wrong_value'); expect(firstChild.get('name')).toBe('new_value_12'); - expect(secondChild().get('name')).toBe('new_value_14'); + expect(secondChild.get('name')).toBe('new_value_14'); firstGrandchild.set('name', { type: DataVariableType, @@ -280,13 +280,13 @@ describe('Collection component', () => { path: 'age', }); expect(firstGrandchild.get('name')).toBe('new_value_12'); - expect(secondGrandchild().get('name')).toBe('new_value_14'); + expect(secondGrandchild.get('name')).toBe('new_value_14'); firstRecord.set('age', 'most_new_value_12'); secondRecord.set('age', 'most_new_value_14'); expect(firstGrandchild.get('name')).toBe('most_new_value_12'); - expect(secondGrandchild().get('name')).toBe('most_new_value_14'); + expect(secondGrandchild.get('name')).toBe('most_new_value_14'); }); test('Updating the value to a different dynamic variable', async () => { @@ -295,13 +295,13 @@ describe('Collection component', () => { path: 'my_data_source_id.user2.user', }); expect(firstChild.get('name')).toBe('user2'); - expect(secondChild().get('name')).toBe('user2'); - expect(thirdChild().get('name')).toBe('user2'); + expect(secondChild.get('name')).toBe('user2'); + expect(thirdChild.get('name')).toBe('user2'); secondRecord.set('user', 'new_value'); expect(firstChild.get('name')).toBe('new_value'); - expect(secondChild().get('name')).toBe('new_value'); - expect(thirdChild().get('name')).toBe('new_value'); + expect(secondChild.get('name')).toBe('new_value'); + expect(thirdChild.get('name')).toBe('new_value'); // @ts-ignore firstGrandchild.set('name', { @@ -309,12 +309,12 @@ describe('Collection component', () => { path: 'my_data_source_id.user2.user', }); expect(firstGrandchild.get('name')).toBe('new_value'); - expect(secondGrandchild().get('name')).toBe('new_value'); + expect(secondGrandchild.get('name')).toBe('new_value'); secondRecord.set('user', 'most_new_value'); expect(firstGrandchild.get('name')).toBe('most_new_value'); - expect(secondGrandchild().get('name')).toBe('most_new_value'); + expect(secondGrandchild.get('name')).toBe('most_new_value'); }); }); @@ -322,9 +322,9 @@ describe('Collection component', () => { let cmp: Component; let firstChild!: Component; let firstGrandchild!: Component; - let secondChild!: () => Component; - let secondGrandchild!: () => Component; - let thirdChild!: () => Component; + let secondChild!: Component; + let secondGrandchild!: Component; + let thirdChild!: Component; beforeEach(() => { const cmpDef = { @@ -368,9 +368,9 @@ describe('Collection component', () => { firstChild = cmp.components().at(0).components().at(0); firstGrandchild = firstChild.components().at(0); - secondChild = () => cmp.components().at(1).components().at(0); - secondGrandchild = () => secondChild().components().at(0); - thirdChild = () => cmp.components().at(2).components().at(0); + secondChild = cmp.components().at(1).components().at(0); + secondGrandchild = secondChild.components().at(0); + thirdChild = cmp.components().at(2).components().at(0); }); test('Evaluating to static value', () => { @@ -379,10 +379,10 @@ describe('Collection component', () => { expect(firstGrandchild.getAttributes()['name']).toBe('user1'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('user1'); - expect(secondChild().getAttributes()['name']).toBe('user2'); - expect(secondChild().getEl()?.getAttribute('name')).toBe('user2'); - expect(secondGrandchild().getAttributes()['name']).toBe('user2'); - expect(secondGrandchild().getEl()?.getAttribute('name')).toBe('user2'); + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('user2'); + expect(secondGrandchild.getAttributes()['name']).toBe('user2'); + expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('user2'); }); test('Watching Records', async () => { @@ -392,32 +392,30 @@ describe('Collection component', () => { expect(firstGrandchild.getAttributes()['name']).toBe('new_user1_value'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('new_user1_value'); - expect(secondChild().getAttributes()['name']).toBe('user2'); - expect(secondGrandchild().getAttributes()['name']).toBe('user2'); + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondGrandchild.getAttributes()['name']).toBe('user2'); }); test('Updating the value to a static value', async () => { firstChild.setAttributes({ name: 'new_content_value' }); expect(firstChild.getAttributes()['name']).toBe('new_content_value'); expect(firstChild.getEl()?.getAttribute('name')).toBe('new_content_value'); - expect(secondChild().getAttributes()['name']).toBe('new_content_value'); - expect(secondChild().getEl()?.getAttribute('name')).toBe('new_content_value'); + expect(secondChild.getAttributes()['name']).toBe('new_content_value'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('new_content_value'); firstRecord.set('user', 'wrong_value'); - secondChild = () => cmp.components().at(1).components().at(0); - secondGrandchild = () => secondChild().components().at(0); expect(firstChild.getAttributes()['name']).toBe('new_content_value'); expect(firstChild.getEl()?.getAttribute('name')).toBe('new_content_value'); - expect(secondChild().getAttributes()['name']).toBe('new_content_value'); - expect(secondChild().getEl()?.getAttribute('name')).toBe('new_content_value'); + expect(secondChild.getAttributes()['name']).toBe('new_content_value'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('new_content_value'); firstGrandchild.setAttributes({ name: 'new_content_value' }); expect(firstGrandchild.getAttributes()['name']).toBe('new_content_value'); - expect(secondGrandchild().getAttributes()['name']).toBe('new_content_value'); + expect(secondGrandchild.getAttributes()['name']).toBe('new_content_value'); firstRecord.set('user', 'wrong_value'); expect(firstGrandchild.getAttributes()['name']).toBe('new_content_value'); - expect(secondGrandchild().getAttributes()['name']).toBe('new_content_value'); + expect(secondGrandchild.getAttributes()['name']).toBe('new_content_value'); }); test('Updating the value to a diffirent collection variable', async () => { @@ -432,8 +430,8 @@ describe('Collection component', () => { }); expect(firstChild.getAttributes()['name']).toBe('12'); expect(firstChild.getEl()?.getAttribute('name')).toBe('12'); - expect(secondChild().getAttributes()['name']).toBe('14'); - expect(secondChild().getEl()?.getAttribute('name')).toBe('14'); + expect(secondChild.getAttributes()['name']).toBe('14'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('14'); firstRecord.set('age', 'new_value_12'); secondRecord.set('age', 'new_value_14'); @@ -443,8 +441,8 @@ describe('Collection component', () => { expect(firstChild.getAttributes()['name']).toBe('new_value_12'); expect(firstChild.getEl()?.getAttribute('name')).toBe('new_value_12'); - expect(secondChild().getAttributes()['name']).toBe('new_value_14'); - expect(secondChild().getEl()?.getAttribute('name')).toBe('new_value_14'); + expect(secondChild.getAttributes()['name']).toBe('new_value_14'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('new_value_14'); firstGrandchild.setAttributes({ name: { @@ -457,14 +455,14 @@ describe('Collection component', () => { }); expect(firstGrandchild.getAttributes()['name']).toBe('new_value_12'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('new_value_12'); - expect(secondGrandchild().getAttributes()['name']).toBe('new_value_14'); - expect(secondGrandchild().getEl()?.getAttribute('name')).toBe('new_value_14'); + expect(secondGrandchild.getAttributes()['name']).toBe('new_value_14'); + expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('new_value_14'); firstRecord.set('age', 'most_new_value_12'); secondRecord.set('age', 'most_new_value_14'); expect(firstGrandchild.getAttributes()['name']).toBe('most_new_value_12'); - expect(secondGrandchild().getAttributes()['name']).toBe('most_new_value_14'); + expect(secondGrandchild.getAttributes()['name']).toBe('most_new_value_14'); }); test('Updating the value to a different dynamic variable', async () => { @@ -477,16 +475,16 @@ describe('Collection component', () => { }); expect(firstChild.getAttributes()['name']).toBe('user2'); expect(firstChild.getEl()?.getAttribute('name')).toBe('user2'); - expect(secondChild().getAttributes()['name']).toBe('user2'); - expect(secondChild().getEl()?.getAttribute('name')).toBe('user2'); - expect(thirdChild().getAttributes()['name']).toBe('user2'); + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('user2'); + expect(thirdChild.getAttributes()['name']).toBe('user2'); secondRecord.set('user', 'new_value'); expect(firstChild.getAttributes()['name']).toBe('new_value'); expect(firstChild.getEl()?.getAttribute('name')).toBe('new_value'); - expect(secondChild().getAttributes()['name']).toBe('new_value'); - expect(secondChild().getEl()?.getAttribute('name')).toBe('new_value'); - expect(thirdChild().getAttributes()['name']).toBe('new_value'); + expect(secondChild.getAttributes()['name']).toBe('new_value'); + expect(secondChild.getEl()?.getAttribute('name')).toBe('new_value'); + expect(thirdChild.getAttributes()['name']).toBe('new_value'); firstGrandchild.setAttributes({ name: { @@ -497,15 +495,15 @@ describe('Collection component', () => { }); expect(firstGrandchild.getAttributes()['name']).toBe('new_value'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('new_value'); - expect(secondGrandchild().getAttributes()['name']).toBe('new_value'); - expect(secondGrandchild().getEl()?.getAttribute('name')).toBe('new_value'); + expect(secondGrandchild.getAttributes()['name']).toBe('new_value'); + expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('new_value'); secondRecord.set('user', 'most_new_value'); expect(firstGrandchild.getAttributes()['name']).toBe('most_new_value'); expect(firstGrandchild.getEl()?.getAttribute('name')).toBe('most_new_value'); - expect(secondGrandchild().getAttributes()['name']).toBe('most_new_value'); - expect(secondGrandchild().getEl()?.getAttribute('name')).toBe('most_new_value'); + expect(secondGrandchild.getAttributes()['name']).toBe('most_new_value'); + expect(secondGrandchild.getEl()?.getAttribute('name')).toBe('most_new_value'); }); }); @@ -551,24 +549,24 @@ describe('Collection component', () => { expect(cmp.getItemsCount()).toBe(3); const firstChild = cmp.components().at(0).components().at(0); - const secondChild = () => cmp.components().at(1).components().at(0); + const secondChild = cmp.components().at(1).components().at(0); expect(firstChild.getAttributes()['attribute_trait']).toBe('user1'); expect(firstChild.getEl()?.getAttribute('attribute_trait')).toBe('user1'); expect(firstChild.get('property_trait')).toBe('user1'); - expect(secondChild().getAttributes()['attribute_trait']).toBe('user2'); - expect(secondChild().getEl()?.getAttribute('attribute_trait')).toBe('user2'); - expect(secondChild().get('property_trait')).toBe('user2'); + expect(secondChild.getAttributes()['attribute_trait']).toBe('user2'); + expect(secondChild.getEl()?.getAttribute('attribute_trait')).toBe('user2'); + expect(secondChild.get('property_trait')).toBe('user2'); firstRecord.set('user', 'new_user1_value'); expect(firstChild.getAttributes()['attribute_trait']).toBe('new_user1_value'); expect(firstChild.getEl()?.getAttribute('attribute_trait')).toBe('new_user1_value'); expect(firstChild.get('property_trait')).toBe('new_user1_value'); - expect(secondChild().getAttributes()['attribute_trait']).toBe('user2'); - expect(secondChild().getEl()?.getAttribute('attribute_trait')).toBe('user2'); - expect(secondChild().get('property_trait')).toBe('user2'); + expect(secondChild.getAttributes()['attribute_trait']).toBe('user2'); + expect(secondChild.getEl()?.getAttribute('attribute_trait')).toBe('user2'); + expect(secondChild.get('property_trait')).toBe('user2'); }); }); @@ -854,18 +852,18 @@ describe('Collection component', () => { const component = components.models[0] as ComponentDataCollection; const firstChild = component.components().at(0).components().at(0); const firstGrandchild = firstChild.components().at(0); - const secondChild = () => component.components().at(1).components().at(0); - const secondGrandchild = () => secondChild().components().at(0); + const secondChild = component.components().at(1).components().at(0); + const secondGrandchild = secondChild.components().at(0); expect(firstChild.get('name')).toBe('user1'); expect(firstChild.getAttributes()['name']).toBe('user1'); expect(firstGrandchild.get('name')).toBe('user1'); expect(firstGrandchild.getAttributes()['name']).toBe('user1'); - expect(secondChild().get('name')).toBe('user2'); - expect(secondChild().getAttributes()['name']).toBe('user2'); - expect(secondGrandchild().get('name')).toBe('user2'); - expect(secondGrandchild().getAttributes()['name']).toBe('user2'); + expect(secondChild.get('name')).toBe('user2'); + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondGrandchild.get('name')).toBe('user2'); + expect(secondGrandchild.getAttributes()['name']).toBe('user2'); firstRecord.set('user', 'new_user1_value'); expect(firstChild.get('name')).toBe('new_user1_value'); @@ -873,10 +871,10 @@ describe('Collection component', () => { expect(firstGrandchild.get('name')).toBe('new_user1_value'); expect(firstGrandchild.getAttributes()['name']).toBe('new_user1_value'); - expect(secondChild().get('name')).toBe('user2'); - expect(secondChild().getAttributes()['name']).toBe('user2'); - expect(secondGrandchild().get('name')).toBe('user2'); - expect(secondGrandchild().getAttributes()['name']).toBe('user2'); + expect(secondChild.get('name')).toBe('user2'); + expect(secondChild.getAttributes()['name']).toBe('user2'); + expect(secondGrandchild.get('name')).toBe('user2'); + expect(secondGrandchild.getAttributes()['name']).toBe('user2'); }); }); From 584fe1df66ee36d128bd40189b0c763913af514a Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 29 Sep 2025 10:31:16 +0300 Subject: [PATCH 29/33] cleanup tests and types --- .../model/data_collection/types.ts | 9 ++- .../src/dom_components/model/Component.ts | 20 +++--- packages/core/src/pages/model/Page.ts | 1 - .../dom_components/model/ComponentWrapper.ts | 69 +++++++++---------- 4 files changed, 46 insertions(+), 53 deletions(-) diff --git a/packages/core/src/data_sources/model/data_collection/types.ts b/packages/core/src/data_sources/model/data_collection/types.ts index c050cf8f11..8a50cc53fb 100644 --- a/packages/core/src/data_sources/model/data_collection/types.ts +++ b/packages/core/src/data_sources/model/data_collection/types.ts @@ -30,11 +30,10 @@ export interface DataCollectionState { export type RootDataType = Array | ObjectAny; -export type DataCollectionStateMap = { - [key: string]: DataCollectionState; -} & { - [keyRootData]?: RootDataType; -}; +export interface DataCollectionStateMap { + [key: string]: DataCollectionState | RootDataType | undefined; + rootData?: RootDataType; +} export interface ComponentDataCollectionProps extends ComponentDefinition { type: typeof DataCollectionType; diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 6b2d10c2b5..7d918b8237 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -59,6 +59,7 @@ import { import { DataWatchersOptions } from './ModelResolverWatcher'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import { checkAndGetSyncableCollectionItemId } from '../../data_sources/utils'; +import { keyRootData } from '../constants'; export interface IComponent extends ExtractMethods {} export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DataWatchersOptions {} @@ -428,7 +429,7 @@ export default class Component extends StyleableModel { } __onStyleChange(newStyles: StyleProps, opts?: UpdateStyleOptions) { - const { em } = this; + const { collectionsStateMap, em } = this; if (!em || opts?.noEvent) return; const styleKeys = keys(newStyles); @@ -437,13 +438,14 @@ export default class Component extends StyleableModel { this.emitWithEditor(ComponentsEvents.styleUpdate, this, pros); styleKeys.forEach((key) => this.emitWithEditor(`${ComponentsEvents.styleUpdateProperty}${key}`, this, pros)); - const collectionsStateMap = this.collectionsStateMap; - const allParentCollectionIds = Object.keys(collectionsStateMap); - if (!allParentCollectionIds.length) return; + const parentCollectionIds = Object.keys(collectionsStateMap).filter((key) => key !== keyRootData); - const isAtInitialPosition = allParentCollectionIds.every( - (key) => collectionsStateMap[key].currentIndex === collectionsStateMap[key].startIndex, - ); + if (parentCollectionIds.length === 0) return; + + const isAtInitialPosition = parentCollectionIds.every((id) => { + const collection = collectionsStateMap[id] as DataCollectionStateMap; + return collection.currentIndex === collection.startIndex; + }); if (!isAtInitialPosition) return; const componentsToUpdate = getSymbolsToUpdate(this); @@ -451,9 +453,7 @@ export default class Component extends StyleableModel { const componentCollectionsState = component.collectionsStateMap; const componentParentCollectionIds = Object.keys(componentCollectionsState); - const isChildOfOriginalCollections = componentParentCollectionIds.every((id) => - allParentCollectionIds.includes(id), - ); + const isChildOfOriginalCollections = componentParentCollectionIds.every((id) => parentCollectionIds.includes(id)); if (isChildOfOriginalCollections) { component.addStyle({ ...newStyles }, { noEvent: true }); diff --git a/packages/core/src/pages/model/Page.ts b/packages/core/src/pages/model/Page.ts index 1a7df7582e..03d8f3d390 100644 --- a/packages/core/src/pages/model/Page.ts +++ b/packages/core/src/pages/model/Page.ts @@ -6,7 +6,6 @@ import ComponentWrapper from '../../dom_components/model/ComponentWrapper'; import EditorModel from '../../editor/model/Editor'; import { CssRuleJSON } from '../../css_composer/model/CssRule'; import { ComponentDefinition } from '../../dom_components/model/types'; -import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; /** @private */ export interface PageProperties { diff --git a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts index 91938b8e2b..384650694c 100644 --- a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts +++ b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts @@ -43,33 +43,41 @@ describe('ComponentWrapper', () => { describe('ComponentWrapper with DataResolver', () => { let em: EditorModel; let dsm: DataSourceManager; - let pagesDataSource: DataSource; + let blogDataSource: DataSource; let wrapper: ComponentWrapper; let firstRecord: DataRecord; - const firstPageData = { id: 'page1', title: 'Title1' }; - const pagesData = [firstPageData, { id: 'page2', title: 'Title2' }, { id: 'page3', title: 'Title3' }]; - const objectData = { - page1: { title: 'page1' }, - page2: { title: 'page2' }, + const firstBlog = { id: 'blog1', title: 'How to Test Components' }; + const blogsData = [ + firstBlog, + { id: 'blog2', title: 'Refactoring for Clarity' }, + { id: 'blog3', title: 'Async Patterns in TS' }, + ]; + + const productsById = { + product1: { title: 'Laptop' }, + product2: { title: 'Smartphone' }, }; beforeEach(() => { ({ em, dsm } = setupTestEditor()); wrapper = em.getWrapper() as ComponentWrapper; - pagesDataSource = dsm.add({ - id: 'pagesDataSource', + blogDataSource = dsm.add({ + id: 'contentDataSource', records: [ - { id: 'pages', data: pagesData }, { - id: 'objectData', - data: objectData, + id: 'blogs', + data: blogsData, + }, + { + id: 'productsById', + data: productsById, }, ], }); - firstRecord = em.DataSources.get('pagesDataSource').getRecord('pages')!; + firstRecord = em.DataSources.get('contentDataSource').getRecord('blogs')!; }); afterEach(() => { @@ -91,47 +99,34 @@ describe('ComponentWrapper', () => { }, })[0]; - test('sets dataResolver and updates wrapper.page/head collectionsStateMap', () => { - wrapper.setDataResolver(createDataResolver('pagesDataSource.pages.data')); - wrapper.resolverCurrentItem = 0; - const stateMap = wrapper.collectionsStateMap; - - expect(stateMap).toHaveProperty(keyRootData); - expect(wrapper.head.collectionsStateMap).toEqual(stateMap); - expect(wrapper.head.collectionsStateMap).toEqual({ [keyRootData]: firstPageData }); - }); - test('children reflect resolved value from dataResolver', () => { - wrapper.setDataResolver(createDataResolver('pagesDataSource.pages.data')); + wrapper.setDataResolver(createDataResolver('contentDataSource.blogs.data')); wrapper.resolverCurrentItem = 0; const child = appendChildWithTitle(); - expect(child.collectionsStateMap).toEqual({ [keyRootData]: firstPageData }); - expect(child.get('title')).toBe(pagesData[0].title); + expect(child.get('title')).toBe(blogsData[0].title); - firstRecord.set('data', [{ id: 'page1', title: 'new_title' }]); - expect(child.get('title')).toBe('new_title'); + firstRecord.set('data', [{ id: 'blog1', title: 'New Blog Title' }]); + expect(child.get('title')).toBe('New Blog Title'); }); test('children update collectionStateMap on wrapper.setDataResolver', () => { const child = appendChildWithTitle(); - wrapper.setDataResolver(createDataResolver('pagesDataSource.pages.data')); + wrapper.setDataResolver(createDataResolver('contentDataSource.blogs.data')); wrapper.resolverCurrentItem = 0; - expect(child.collectionsStateMap).toEqual({ [keyRootData]: firstPageData }); - expect(child.get('title')).toBe(pagesData[0].title); + expect(child.get('title')).toBe(blogsData[0].title); - firstRecord.set('data', [{ id: 'page1', title: 'new_title' }]); - expect(child.get('title')).toBe('new_title'); + firstRecord.set('data', [{ id: 'blog1', title: 'Updated Title' }]); + expect(child.get('title')).toBe('Updated Title'); }); - test('wrapper should handle objects as collection state ', () => { - wrapper.setDataResolver(createDataResolver('pagesDataSource.objectData.data')); - wrapper.resolverCurrentItem = 'page1'; + test('wrapper should handle objects as collection state', () => { + wrapper.setDataResolver(createDataResolver('contentDataSource.productsById.data')); + wrapper.resolverCurrentItem = 'product1'; const child = appendChildWithTitle('title'); - expect(child.collectionsStateMap).toEqual({ [keyRootData]: objectData.page1 }); - expect(child.get('title')).toBe(objectData.page1.title); + expect(child.get('title')).toBe(productsById.product1.title); }); }); }); From 7d80b0019e7d73c39dbaed114f30b0e300739059 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 29 Sep 2025 14:13:41 +0300 Subject: [PATCH 30/33] fix performance issue for the new wrapper datasource --- .../dom_components/model/ComponentWrapper.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index e7d00a82fc..7bc780ce93 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -17,6 +17,7 @@ type ResolverCurrentItemType = string | number; export default class ComponentWrapper extends ComponentWithCollectionsState { dataSourceWatcher?: DataResolverListener; private _resolverCurrentItem?: ResolverCurrentItemType; + private _isWatchingCollectionStateMap = false; get defaults() { return { @@ -47,10 +48,9 @@ export default class ComponentWrapper extends ComponentWithCollectionsState { - this.onCollectionsStateMapUpdate(this.getCollectionsStateMap()); - this.listenToDataSource(); + this.on(`change:dataResolver`, (_, value) => { + const hasResolver = !isUndefined(value); + + if (hasResolver && !this._isWatchingCollectionStateMap) { + this._isWatchingCollectionStateMap = true; + this.syncComponentsCollectionState(); + this.onCollectionsStateMapUpdate(this.getCollectionsStateMap()); + this.listenToDataSource(); + } else if (!hasResolver && this._isWatchingCollectionStateMap) { + this._isWatchingCollectionStateMap = false; + this.stopSyncComponentCollectionState(); + } }); this.listenToDataSource(); From 470c451b377def248bddc04fc571d94e68484405 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Mon, 29 Sep 2025 14:14:19 +0300 Subject: [PATCH 31/33] Undo updating component with datacolection tests --- .../model/data_collection/ComponentDataCollection.ts | 4 ++-- .../ComponentDataCollectionWithDataVariable.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index 902b9ad459..0362ef9d45 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -169,9 +169,9 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta private rebuildChildrenFromCollection() { const items = this.getDataSourceItems(); - const itemsCount = size(items); + const { totalItems } = this.resolveCollectionConfig(items); - if (itemsCount === this.components().length) { + if (totalItems === this.components().length) { this.onCollectionsStateMapUpdate(this.collectionsStateMap); return; } diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts index fd8bcccf8e..764667c763 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionWithDataVariable.ts @@ -255,19 +255,19 @@ describe('Collection variable components', () => { 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); + const secondChild = component.components().at(1); + const secondGrandchild = secondChild.components().at(0); expect(firstGrandchild.getInnerHTML()).toBe('user1'); - expect(secondGrandchild().getInnerHTML()).toBe('user2'); + expect(secondGrandchild.getInnerHTML()).toBe('user2'); firstRecord.set('user', 'new_user1_value'); expect(firstGrandchild.getInnerHTML()).toBe('new_user1_value'); - expect(secondGrandchild().getInnerHTML()).toBe('user2'); + 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'); + expect(secondGrandchild.getInnerHTML()).toBe('new_user2_value'); }); }); }); From ac43e1e93f0a4a6cda1abb794522cb040c659c73 Mon Sep 17 00:00:00 2001 From: mohamedsalem401 Date: Fri, 3 Oct 2025 13:44:45 +0300 Subject: [PATCH 32/33] apply comments --- .../data_sources/model/ComponentWithCollectionsState.ts | 7 ++++--- .../model/data_collection/ComponentDataCollection.ts | 2 +- packages/core/src/dom_components/model/ComponentWrapper.ts | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts index effaecb23d..13f76fea49 100644 --- a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -41,15 +41,16 @@ export default class ComponentWithCollectionsState extends Com return this.set('dataResolver', dataResolver); } - getDataResolver(): DataResolverType | undefined { + get dataResolverProps(): DataResolverType | undefined { return this.get('dataResolver'); } protected listenToDataSource() { - const path = this.dataSourcePath; + const path = this.dataResolverPath; if (!path) return; const { em, collectionsStateMap } = this; + this.dataSourceWatcher?.destroy(); this.dataSourceWatcher = new DataResolverListener({ em, resolver: new DataVariable({ type: DataVariableType, path }, { em, collectionsStateMap }), @@ -69,7 +70,7 @@ export default class ComponentWithCollectionsState extends Com return this.get('dataResolver'); } - protected get dataSourcePath(): string | undefined { + protected get dataResolverPath(): string | undefined { return this.dataSourceProps?.path; } diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index 0362ef9d45..eed5b87979 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -295,7 +295,7 @@ export default class ComponentDataCollection extends ComponentWithCollectionsSta } private get collectionId() { - return this.getDataResolver()?.collectionId ?? ''; + return this.dataResolverProps?.collectionId ?? ''; } static isComponent(el: HTMLElement) { diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index 7bc780ce93..6c89c8c0c8 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -47,7 +47,7 @@ export default class ComponentWrapper extends ComponentWithCollectionsState Date: Fri, 10 Oct 2025 14:44:24 +0400 Subject: [PATCH 33/33] Skip same path update --- .../core/src/data_sources/model/DataResolverListener.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/src/data_sources/model/DataResolverListener.ts b/packages/core/src/data_sources/model/DataResolverListener.ts index bf1c805422..f2a211904d 100644 --- a/packages/core/src/data_sources/model/DataResolverListener.ts +++ b/packages/core/src/data_sources/model/DataResolverListener.ts @@ -102,11 +102,12 @@ export default class DataResolverListener { dataListeners.push( this.createListener(em.DataSources.all, 'add remove reset', onChangeAndRewatch), this.createListener(em, `${DataSourcesEvents.path}:${normPath}`), - ); - - dataListeners.push( this.createListener(em, DataSourcesEvents.path, ({ path: eventPath }: { path: string }) => { - if (eventPath.startsWith(path)) { + if ( + // Skip same path as it's already handled be the listener above + eventPath !== path && + eventPath.startsWith(path) + ) { this.onChange(); } }),