Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
cc7b2de
Add tests for component wrapper
mohamedsalem401 Sep 3, 2025
e7aede4
Refactor component data collection
mohamedsalem401 Sep 3, 2025
d81686d
Add data resolver to wrapper component
mohamedsalem401 Sep 3, 2025
0cb1a83
Fix types
mohamedsalem401 Sep 3, 2025
af2c99f
Add collection data source to page
mohamedsalem401 Sep 3, 2025
efd75e3
Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into pages…
mohamedsalem401 Sep 8, 2025
436c064
refactor get and set DataResolver to componentWrapper
mohamedsalem401 Sep 8, 2025
59096ea
Rename key to __rootData
mohamedsalem401 Sep 8, 2025
7df0c9f
add resolverCurrentItem
mohamedsalem401 Sep 8, 2025
4ba97dd
Make _resolverCurrentItem private
mohamedsalem401 Sep 8, 2025
1bf25c0
update ComponentWrapper tests
mohamedsalem401 Sep 8, 2025
b6181ad
Fix componentWithCollectionsState
mohamedsalem401 Sep 8, 2025
608fdd9
remove collectionsStateMap from Page
mohamedsalem401 Sep 8, 2025
9e68c92
update component wrapper tests
mohamedsalem401 Sep 9, 2025
edd9516
fix component wrapper tests
mohamedsalem401 Sep 9, 2025
da8daa8
return a copy of records for DataSource.getPath
mohamedsalem401 Sep 10, 2025
094b0da
Move all collection listeners to component with collection state
mohamedsalem401 Sep 10, 2025
b96dee5
fix style sync in collection items
mohamedsalem401 Sep 10, 2025
2f8c198
fix loop issue
mohamedsalem401 Sep 10, 2025
7772c7d
update data collection tests
mohamedsalem401 Sep 10, 2025
eb9b4ff
cleanup
mohamedsalem401 Sep 10, 2025
4499b06
update collection statemap on wrapper change
mohamedsalem401 Sep 10, 2025
6a71c87
Add object test data for wrapper data resolver
mohamedsalem401 Sep 10, 2025
fb84c3c
cleanup
mohamedsalem401 Sep 10, 2025
b82eb8c
up unit test
mohamedsalem401 Sep 10, 2025
aef8269
remove duplicated code
mohamedsalem401 Sep 29, 2025
f26a365
cleanup event path
mohamedsalem401 Sep 29, 2025
d2c2579
update test data to better names
mohamedsalem401 Sep 29, 2025
56f46fd
improve component data collection performance
mohamedsalem401 Sep 29, 2025
584fe1d
cleanup tests and types
mohamedsalem401 Sep 29, 2025
b600eb6
Merge branch 'dev' of https://github.com/GrapesJS/grapesjs into pages…
mohamedsalem401 Sep 29, 2025
7d80b00
fix performance issue for the new wrapper datasource
mohamedsalem401 Sep 29, 2025
470c451
Undo updating component with datacolection tests
mohamedsalem401 Sep 29, 2025
ac43e1e
apply comments
mohamedsalem401 Oct 3, 2025
4704d05
Skip same path update
artf Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/core/src/data_sources/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ export default class DataSourceManager extends ItemManagerModule<ModuleConfig, D
acc[ds.id] = ds.records.reduce((accR, dr, i) => {
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);
Expand Down
120 changes: 120 additions & 0 deletions packages/core/src/data_sources/model/ComponentWithCollectionsState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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 DataSource from './DataSource';
import { isArray } from 'underscore';

export type DataVariableMap = Record<string, DataVariableProps>;

export type DataSourceRecords = DataVariableProps[] | DataVariableMap;

export default class ComponentWithCollectionsState<DataResolverType> 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);
}

setDataResolver(dataResolver: DataResolverType | undefined) {
return this.set('dataResolver', dataResolver);
}

get dataResolverProps(): DataResolverType | undefined {
return this.get('dataResolver');
}

protected listenToDataSource() {
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 }),
onUpdate: () => this.onDataSourceChange(),
});
}

protected listenToPropsChange() {
this.on(`change:dataResolver`, () => {
this.listenToDataSource();
});

this.listenToDataSource();
}

protected get dataSourceProps(): DataVariableProps | undefined {
return this.get('dataResolver');
}

protected get dataResolverPath(): string | undefined {
return this.dataSourceProps?.path;
}

protected onDataSourceChange() {
this.onCollectionsStateMapUpdate(this.collectionsStateMap);
}

protected getDataSourceItems(): DataSourceRecords {
const dataSourceProps = this.dataSourceProps;
if (!dataSourceProps) return [];
const items = this.listDataSourceItems(dataSourceProps);
if (items && isArray(items)) {
return items;
}

const clone = { ...items };
return clone;
}

protected listDataSourceItems(dataSource: DataSource | DataVariableProps): DataSourceRecords {
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) {
return isArray(items) ? index : Object.keys(items)[index];
}

private removePropsListeners() {
this.off(`change:dataResolver`);
this.dataSourceWatcher?.destroy();
this.dataSourceWatcher = undefined;
}

destroy(options?: ObjectAny): false | JQueryXHR {
this.removePropsListeners();
return super.destroy(options);
}
}
17 changes: 15 additions & 2 deletions packages/core/src/data_sources/model/DataResolverListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface DataResolverListenerProps {
}

interface ListenerWithCallback extends DataSourceListener {
callback: () => void;
callback: (opts?: any) => void;
}

export default class DataResolverListener {
Expand All @@ -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 };
}

Expand Down Expand Up @@ -98,6 +102,15 @@ export default class DataResolverListener {
dataListeners.push(
this.createListener(em.DataSources.all, 'add remove reset', onChangeAndRewatch),
this.createListener(em, `${DataSourcesEvents.path}:${normPath}`),
this.createListener(em, DataSourcesEvents.path, ({ path: eventPath }: { path: string }) => {
if (
// Skip same path as it's already handled be the listener above
eventPath !== path &&
eventPath.startsWith(path)
) {
this.onChange();
}
}),
);

return dataListeners;
Expand Down
36 changes: 25 additions & 11 deletions packages/core/src/data_sources/model/DataVariable.ts
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -134,44 +140,52 @@ export default class DataVariable extends Model<DataVariableProps> {
);
}

private resolveCollectionVariable(): unknown {
private resolveCollectionVariable() {
const { em, collectionsStateMap } = this;
return DataVariable.resolveCollectionVariable(this.attributes, { em, collectionsStateMap });
}

static resolveCollectionVariable(
dataResolverProps: {
params: {
collectionId?: string;
variableType?: DataCollectionStateType;
path?: string;
defaultValue?: string;
},
opts: DataVariableOptions,
): unknown {
const { collectionId = '', variableType, path, defaultValue = '' } = dataResolverProps;
const { em, collectionsStateMap } = opts;
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) {
const root = collectionItem as RootDataType;
return path ? root?.[path as keyof RootDataType] : root;
}

if (!variableType) {
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 as DataCollectionState, path, collectionId, em);
}

const state = collectionItem as DataCollectionState;
return state[variableType] ?? defaultValue;
}

private static resolveCurrentItem(
collectionItem: DataCollectionState,
path: string | undefined,
collectionId: string,
em: EditorModel,
): unknown {
) {
const currentItem = collectionItem.currentItem;
if (!currentItem) {
em.logError(`Current item is missing for collection: ${collectionId}`);
Expand Down
Loading