From 754e2f4f5689dc404f5be786c85c0b3c33c0aef5 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 17 Nov 2025 13:53:25 +0400 Subject: [PATCH 1/4] Fixes and improvements for datasources (#6649) * Improve data source TS * Skip running updateSymbolOverride on non-symbol components * Refactor data types * Update default root resolver item * Resolve direct data sources in ComponentWrapper * Add updateDebug event * Fix data resolver update logic * Fix id removal in ComponentDataCollection and tests * Clone CSS rules pasted cross components * Fix attributes/styles collide in data sources --- .../core/src/css_composer/model/CssRule.ts | 4 +- packages/core/src/data_sources/index.ts | 32 +++++++++- .../model/ComponentWithCollectionsState.ts | 3 +- .../model/ComponentWithDataResolver.ts | 8 --- .../core/src/data_sources/model/DataSource.ts | 5 +- .../src/data_sources/model/DataVariable.ts | 3 +- .../ComponentDataCondition.ts | 19 +++--- .../model/conditional_variables/constants.ts | 2 - .../ComponentDataCollection.ts | 31 ++++----- .../model/data_collection/constants.ts | 2 - .../model/data_collection/types.ts | 8 +-- packages/core/src/data_sources/types.ts | 9 +++ packages/core/src/data_sources/utils.ts | 11 ++-- packages/core/src/dom_components/index.ts | 64 +++++++++---------- .../src/dom_components/model/Component.ts | 23 +++++-- .../dom_components/model/ComponentWrapper.ts | 43 +++++++------ .../src/dom_components/model/Components.ts | 52 +++++++-------- .../model/ModelDataResolverWatchers.ts | 18 ++++-- packages/core/src/dom_components/types.ts | 1 + .../domain_abstract/model/StyleableModel.ts | 6 +- packages/core/src/editor/model/Editor.ts | 7 +- packages/core/src/editor/types.ts | 1 + packages/core/src/index.ts | 2 + packages/core/src/pages/index.ts | 5 +- packages/core/test/common.ts | 12 ++-- .../data_sources/dynamic_values/attributes.ts | 57 +++++++++++++++++ .../ComponentDataCondition.ts | 8 +-- ...ComponentDataCollection.getters-setters.ts | 10 +-- .../ComponentDataCollection.ts | 47 ++++++++++++-- ...ComponentDataCollectionWithDataVariable.ts | 14 ++-- .../ComponentDataCollection.ts.snap | 40 ++++++++++++ ...nentDataCollectionWithDataVariable.ts.snap | 54 ++++++++++++++++ .../nestedComponentDataCollections.ts.snap | 15 +++++ .../nestedComponentDataCollections.ts | 20 ++++-- .../specs/dom_components/model/Component.ts | 24 +++++++ .../dom_components/model/ComponentWrapper.ts | 56 ++++++++++++---- 36 files changed, 515 insertions(+), 201 deletions(-) delete mode 100644 packages/core/src/data_sources/model/conditional_variables/constants.ts diff --git a/packages/core/src/css_composer/model/CssRule.ts b/packages/core/src/css_composer/model/CssRule.ts index 00a732966..b727e30ad 100644 --- a/packages/core/src/css_composer/model/CssRule.ts +++ b/packages/core/src/css_composer/model/CssRule.ts @@ -129,10 +129,10 @@ export default class CssRule extends StyleableModel { this.on('change', this.__onChange); } - __onChange(m: CssRule, opts: any) { + __onChange(rule: CssRule, options: any) { const { em } = this; const changed = this.changedAttributes(); - changed && !isEmptyObj(changed) && em?.changesUp(opts); + changed && !isEmptyObj(changed) && em?.changesUp(options, { rule, changed, options }); } clone(): typeof this { diff --git a/packages/core/src/data_sources/index.ts b/packages/core/src/data_sources/index.ts index ebe2f2500..09560ffe8 100644 --- a/packages/core/src/data_sources/index.ts +++ b/packages/core/src/data_sources/index.ts @@ -21,21 +21,34 @@ * @module DataSources */ +import { Events } from 'backbone'; import { isEmpty } from 'underscore'; import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; import { AddOptions, collectionEvents, ObjectAny, RemoveOptions } from '../common'; import EditorModel from '../editor/model/Editor'; import { get, set, stringToPath } from '../utils/mixins'; import defConfig, { DataSourcesConfig } from './config/config'; +import { AnyTypeOperation } from './model/conditional_variables/operators/AnyTypeOperator'; +import { BooleanOperation } from './model/conditional_variables/operators/BooleanOperator'; +import { NumberOperation } from './model/conditional_variables/operators/NumberOperator'; +import { StringOperation } from './model/conditional_variables/operators/StringOperator'; +import { DataCollectionStateType } from './model/data_collection/types'; import DataRecord from './model/DataRecord'; import DataSource from './model/DataSource'; import DataSources from './model/DataSources'; -import { DataSourcesEvents, DataSourceProps, DataRecordProps } from './types'; -import { Events } from 'backbone'; +import { DataComponentTypes, DataRecordProps, DataSourceProps, DataSourcesEvents } from './types'; export default class DataSourceManager extends ItemManagerModule { storageKey = 'dataSources'; events = DataSourcesEvents; + dataComponentTypes = DataComponentTypes; + dataCollectionStateTypes = DataCollectionStateType; + dataOperationTypes = { + any: AnyTypeOperation, + boolean: BooleanOperation, + number: NumberOperation, + string: StringOperation, + }; destroy(): void {} constructor(em: EditorModel) { @@ -74,6 +87,16 @@ export default class DataSourceManager extends ItemManagerModule} + * @example + * const ds = dsm.getAll(); + */ + getAll() { + return [...this.all.models]; + } + /** * Get value from data sources by path. * @param {String} path Path to value. @@ -211,7 +234,10 @@ export default class DataSourceManager extends ItemManagerModule em.changesUp(o || c)); + em.listenTo(all, collectionEvents, (dataSource, c, o) => { + const options = o || c; + em.changesUp(options, { dataSource, options }); + }); this.em.UndoManager.add(all); } } diff --git a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts index 1791bd8ea..a7e6ed325 100644 --- a/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts +++ b/packages/core/src/data_sources/model/ComponentWithCollectionsState.ts @@ -83,8 +83,9 @@ export default class ComponentWithCollectionsState extends Com } protected getDataSourceItems(): DataSourceRecords { - const dataSourceProps = this.dataSourceProps; + const { dataSourceProps } = this; if (!dataSourceProps) return []; + const items = this.listDataSourceItems(dataSourceProps); if (items && isArray(items)) { return items; diff --git a/packages/core/src/data_sources/model/ComponentWithDataResolver.ts b/packages/core/src/data_sources/model/ComponentWithDataResolver.ts index 89f720726..eb82d94cb 100644 --- a/packages/core/src/data_sources/model/ComponentWithDataResolver.ts +++ b/packages/core/src/data_sources/model/ComponentWithDataResolver.ts @@ -55,14 +55,6 @@ export abstract class ComponentWithDataResolver ext } protected listenToPropsChange() { - this.listenTo( - this.dataResolver, - 'change', - (() => { - this.__changesUp({ m: this }); - }).bind(this), - ); - this.on('change:dataResolver', () => { // @ts-ignore this.dataResolver.set(this.get('dataResolver')); diff --git a/packages/core/src/data_sources/model/DataSource.ts b/packages/core/src/data_sources/model/DataSource.ts index 15bf1757f..9d9909738 100644 --- a/packages/core/src/data_sources/model/DataSource.ts +++ b/packages/core/src/data_sources/model/DataSource.ts @@ -316,7 +316,8 @@ export default class DataSource = T extends DataVaria ? DataCondition : never; +export enum DataComponentTypes { + variable = 'data-variable', + condition = 'data-condition', + conditionTrue = 'data-condition-true-content', + conditionFalse = 'data-condition-false-content', + collection = 'data-collection', + collectionItem = 'data-collection-item', +} + export interface DataRecordProps extends ObjectAny { /** * Record id. diff --git a/packages/core/src/data_sources/utils.ts b/packages/core/src/data_sources/utils.ts index 005b2075f..31c2f0047 100644 --- a/packages/core/src/data_sources/utils.ts +++ b/packages/core/src/data_sources/utils.ts @@ -1,12 +1,10 @@ import EditorModel from '../editor/model/Editor'; -import { DataResolver, DataResolverProps, ResolverFromProps } from './types'; +import { DataComponentTypes, DataResolver, DataResolverProps, ResolverFromProps } from './types'; import { DataCollectionStateMap } from './model/data_collection/types'; -import { DataCollectionItemType } from './model/data_collection/constants'; import { DataConditionType, DataCondition } from './model/conditional_variables/DataCondition'; import DataVariable, { DataVariableProps, DataVariableType } from './model/DataVariable'; import { ComponentDefinition, ComponentOptions } from '../dom_components/model/types'; import { serialize } from '../utils/mixins'; -import { DataConditionIfFalseType, DataConditionIfTrueType } from './model/conditional_variables/constants'; import { getSymbolMain } from '../dom_components/model/SymbolUtils'; import Component from '../dom_components/model/Component'; @@ -85,7 +83,12 @@ export const ensureComponentInstance = ( }; export const isComponentDataOutputType = (type: string | undefined) => { - return !!type && [DataCollectionItemType, DataConditionIfTrueType, DataConditionIfFalseType].includes(type); + return ( + !!type && + [DataComponentTypes.collectionItem, DataComponentTypes.conditionTrue, DataComponentTypes.conditionFalse].includes( + type as DataComponentTypes, + ) + ); }; export function enumToArray(enumObj: any) { diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 46a7b5f46..807492318 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -55,13 +55,23 @@ */ import { debounce, isArray, isEmpty, isFunction, isString, isSymbol, result } from 'underscore'; import { ItemManagerModule } from '../abstract/Module'; +import { BlockProperties } from '../block_manager/model/Block'; import { ObjectAny } from '../common'; +import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; +import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition'; +import ComponentDataOutput from '../data_sources/model/conditional_variables/ComponentDataOutput'; +import ComponentDataCollection from '../data_sources/model/data_collection/ComponentDataCollection'; +import { DataComponentTypes } from '../data_sources/types'; +import ComponentDataCollectionView from '../data_sources/view/ComponentDataCollectionView'; +import ComponentDataConditionView from '../data_sources/view/ComponentDataConditionView'; +import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; import EditorModel from '../editor/model/Editor'; import { isComponent } from '../utils/mixins'; import defConfig, { DomComponentsConfig } from './config/config'; import Component, { IComponent, keyUpdate, keyUpdateInside } from './model/Component'; import ComponentComment from './model/ComponentComment'; import ComponentFrame from './model/ComponentFrame'; +import ComponentHead, { type as typeHead } from './model/ComponentHead'; import ComponentImage from './model/ComponentImage'; import ComponentLabel from './model/ComponentLabel'; import ComponentLink from './model/ComponentLink'; @@ -80,6 +90,18 @@ import ComponentTextNode from './model/ComponentTextNode'; import ComponentVideo from './model/ComponentVideo'; import ComponentWrapper from './model/ComponentWrapper'; import Components from './model/Components'; +import { + detachSymbolInstance, + getSymbolInstances, + getSymbolMain, + getSymbolsToUpdate, + getSymbolTop, + isSymbol as isSymbolComponent, + isSymbolInstance, + isSymbolMain, + isSymbolRoot, +} from './model/SymbolUtils'; +import Symbols from './model/Symbols'; import { AddComponentsOption, ComponentAdd, @@ -87,6 +109,7 @@ import { ComponentDefinitionDefined, ComponentStackItem, } from './model/types'; +import { ComponentsEvents, SymbolInfo } from './types'; import ComponentCommentView from './view/ComponentCommentView'; import ComponentFrameView from './view/ComponentFrameView'; import ComponentImageView from './view/ComponentImageView'; @@ -107,35 +130,6 @@ import ComponentVideoView from './view/ComponentVideoView'; import ComponentView, { IComponentView } from './view/ComponentView'; import ComponentWrapperView from './view/ComponentWrapperView'; import ComponentsView from './view/ComponentsView'; -import ComponentHead, { type as typeHead } from './model/ComponentHead'; -import { - getSymbolMain, - getSymbolInstances, - getSymbolsToUpdate, - isSymbolMain, - isSymbolInstance, - detachSymbolInstance, - isSymbolRoot, - isSymbol as isSymbolComponent, - getSymbolTop, -} from './model/SymbolUtils'; -import { ComponentsEvents, SymbolInfo } from './types'; -import Symbols from './model/Symbols'; -import { BlockProperties } from '../block_manager/model/Block'; -import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; -import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; -import { DataVariableType } from '../data_sources/model/DataVariable'; -import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition'; -import ComponentDataConditionView from '../data_sources/view/ComponentDataConditionView'; -import ComponentDataCollection from '../data_sources/model/data_collection/ComponentDataCollection'; -import { DataCollectionItemType, DataCollectionType } from '../data_sources/model/data_collection/constants'; -import ComponentDataCollectionView from '../data_sources/view/ComponentDataCollectionView'; -import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition'; -import { - DataConditionIfFalseType, - DataConditionIfTrueType, -} from '../data_sources/model/conditional_variables/constants'; -import ComponentDataOutput from '../data_sources/model/conditional_variables/ComponentDataOutput'; export type ComponentEvent = | 'component:create' @@ -202,32 +196,32 @@ export interface CanMoveResult { export default class ComponentManager extends ItemManagerModule { componentTypes: ComponentStackItem[] = [ { - id: DataCollectionItemType, + id: DataComponentTypes.collectionItem, model: ComponentDataOutput, view: ComponentView, }, { - id: DataConditionIfTrueType, + id: DataComponentTypes.conditionTrue, model: ComponentDataOutput, view: ComponentView, }, { - id: DataConditionIfFalseType, + id: DataComponentTypes.conditionFalse, model: ComponentDataOutput, view: ComponentView, }, { - id: DataCollectionType, + id: DataComponentTypes.collection, model: ComponentDataCollection, view: ComponentDataCollectionView, }, { - id: DataConditionType, + id: DataComponentTypes.condition, model: ComponentDataCondition, view: ComponentDataConditionView, }, { - id: DataVariableType, + id: DataComponentTypes.variable, model: ComponentDataVariable, view: ComponentDataVariableView, }, diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index ad2ca2782..345dcff3f 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -431,7 +431,7 @@ export default class Component extends StyleableModel { ['status', 'open', 'toolbar', 'traits'].forEach((name) => delete changed[name]); // Propagate component prop changes if (!isEmptyObj(changed)) { - this.__changesUp(opts); + this.__changesUp(opts, { changed }); this.__propSelfToParent({ component: this, changed, options: opts }); } } @@ -469,9 +469,11 @@ export default class Component extends StyleableModel { }); } - __changesUp(opts: any) { + __changesUp(options: any, data: Record = {}) { const { em, frame } = this; - [frame, em].forEach((md) => md && md.changesUp(opts)); + [frame, em].forEach((md) => { + md?.changesUp(options, { component: this, options, ...data }); + }); } __propSelfToParent(props: any) { @@ -1654,7 +1656,6 @@ export default class Component extends StyleableModel { delete obj[keySymbol]; delete obj[keySymbolOvrd]; delete obj[keySymbols]; - delete obj.attributes.id; } if (!opts.fromUndo) { @@ -2134,10 +2135,15 @@ export default class Component extends StyleableModel { components: ComponentDefinitionDefined | ComponentDefinitionDefined[], styles: CssRuleJSON[] = [], list: ObjectAny = {}, - opts: { keepIds?: string[]; idMap?: PrevToNewIdMap } = {}, + opts: { + keepIds?: string[]; + idMap?: PrevToNewIdMap; + updatedIds?: Record; + } = {}, ) { + opts.updatedIds = opts.updatedIds || {}; const comps = isArray(components) ? components : [components]; - const { keepIds = [], idMap = {} } = opts; + const { keepIds = [], idMap = {}, updatedIds } = opts; comps.forEach((comp) => { comp.attributes; const { attributes = {}, components } = comp; @@ -2146,6 +2152,7 @@ export default class Component extends StyleableModel { // Check if we have collisions with current components if (id && list[id] && keepIds.indexOf(id) < 0) { const newId = Component.getIncrementId(id, list); + updatedIds[id] = updatedIds[id] ? [...updatedIds[id], comp] : [comp]; idMap[id] = newId; attributes.id = newId; // Update passed styles @@ -2160,5 +2167,9 @@ export default class Component extends StyleableModel { components && Component.checkId(components, styles, list, opts); }); + + return { + updatedIds: opts.updatedIds, + }; } } diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index 6c89c8c0c..cb6271444 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -1,28 +1,27 @@ -import { isUndefined } from 'underscore'; -import { attrToString } from '../../utils/dom'; -import Component from './Component'; -import ComponentHead, { type as typeHead } from './ComponentHead'; -import { ComponentOptions, ComponentProperties, ToHTMLOptions } from './types'; -import Components from './Components'; +import { isNumber, isString, isUndefined } from 'underscore'; +import ComponentWithCollectionsState from '../../data_sources/model/ComponentWithCollectionsState'; 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 { attrToString } from '../../utils/dom'; import { keyRootData } from '../constants'; +import Component from './Component'; +import ComponentHead, { type as typeHead } from './ComponentHead'; +import Components from './Components'; +import { ComponentOptions, ComponentProperties, ToHTMLOptions } from './types'; type ResolverCurrentItemType = string | number; export default class ComponentWrapper extends ComponentWithCollectionsState { dataSourceWatcher?: DataResolverListener; - private _resolverCurrentItem?: ResolverCurrentItemType; + private _resolverCurrentItem?: ResolverCurrentItemType = 0; private _isWatchingCollectionStateMap = false; get defaults() { return { // @ts-ignore ...super.defaults, + dataResolver: null, tagName: 'body', removable: false, copyable: false, @@ -130,6 +129,10 @@ export default class ComponentWrapper extends ComponentWithCollectionsState; + visitedCmps?: Record; keepIds?: string[]; updateOptions?: { onAttributes?: (props: ResetCommonUpdateProps & { attributes: Record }) => void; @@ -161,32 +161,11 @@ Component> { resetFromString(input = '', opts: ResetFromStringOptions = {}) { opts.keepIds = getComponentIds(this); const { domc, em, parent } = this; - const cssc = em?.Css; const allByID = domc?.allById() || {}; - const parsed = this.parseString(input, opts); + const parsed = this.parseString(input, { ...opts, cloneRules: true }); const fromDefOpts = { skipViewUpdate: true, ...opts }; const newCmps = getComponentsFromDefs(parsed, allByID, fromDefOpts); - const { visitedCmps = {} } = fromDefOpts; - - // Clone styles for duplicated components - Object.keys(visitedCmps).forEach((id) => { - const cmps = visitedCmps[id]; - if (cmps.length) { - // Get all available rules of the component - const rulesToClone = cssc?.getRules(`#${id}`) || []; - - if (rulesToClone.length) { - cmps.forEach((cmp) => { - rulesToClone.forEach((rule) => { - const newRule = rule.clone(); - // @ts-ignore - newRule.set('selectors', [`#${cmp.attributes.id}`]); - cssc!.getAll().add(newRule); - }); - }); - } - } - }); + Components.cloneCssRules(em, fromDefOpts.visitedCmps); this.reset(newCmps, opts as any); em?.trigger('component:content', parent, opts, input); @@ -318,7 +297,8 @@ Component> { } // We need this to avoid duplicate IDs - Component.checkId(components, parsed.css, domc!.componentsById, opt); + const result = Component.checkId(components, parsed.css, domc!.componentsById, opt); + opt.cloneRules && Components.cloneCssRules(em, result.updatedIds); if (parsed.css && cssc && !opt.temporary) { const { at, ...optsToPass } = opt; @@ -447,4 +427,26 @@ Component> { } } } + + static cloneCssRules(em: EditorModel, cmpsMap: Record = {}) { + const { Css } = em; + Object.keys(cmpsMap).forEach((id) => { + const cmps = cmpsMap[id]; + if (cmps.length) { + // Get all available rules of the component + const rulesToClone = (Css.getRules(`#${id}`) || []).filter((rule) => !isEmpty(rule.attributes.style)); + + if (rulesToClone.length) { + const rules = Css.getAll(); + cmps.forEach((cmp) => { + rulesToClone.forEach((rule) => { + const newRule = rule.clone(); + newRule.set('selectors', [`#${cmp.attributes.id}`] as any); + rules.add(newRule); + }); + }); + } + } + }); + } } diff --git a/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts b/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts index f6bc69c90..831ac72c0 100644 --- a/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts +++ b/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts @@ -5,8 +5,8 @@ import { DataWatchersOptions, WatchableModel, } from './ModelResolverWatcher'; -import { getSymbolsToUpdate } from './SymbolUtils'; -import Component from './Component'; +import { getSymbolsToUpdate, isSymbol } from './SymbolUtils'; +import Component, { keySymbolOvrd } from './Component'; import { StyleableModelProperties } from '../../domain_abstract/model/StyleableModel'; import { isEmpty, isObject } from 'underscore'; @@ -166,8 +166,8 @@ export class ModelDataResolverWatchers { } private updateSymbolOverride() { - const model = this.model; - if (!this.isComponent(model)) return; + const { model } = this; + if (!this.isComponent(model) || !isSymbol(model)) return; const isCollectionItem = !!Object.keys(model?.collectionsStateMap ?? {}).length; if (!isCollectionItem) return; @@ -187,7 +187,15 @@ export class ModelDataResolverWatchers { } private filterProps(props: ObjectAny) { - const excludedFromEvaluation = ['components', 'dataResolver', keyDataValues]; + const excludedFromEvaluation = [ + 'components', + 'dataResolver', + 'status', + 'state', + 'open', + keySymbolOvrd, + keyDataValues, + ]; const filteredProps = Object.fromEntries( Object.entries(props).filter(([key]) => !excludedFromEvaluation.includes(key)), ); diff --git a/packages/core/src/dom_components/types.ts b/packages/core/src/dom_components/types.ts index 4f62ecfe8..d0c5782fd 100644 --- a/packages/core/src/dom_components/types.ts +++ b/packages/core/src/dom_components/types.ts @@ -19,6 +19,7 @@ export interface SymbolInfo { export interface ParseStringOptions extends AddOptions, OptionAsDocument, WithHTMLParserOptions { keepIds?: string[]; + cloneRules?: boolean; } export enum ComponentsEvents { diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index 8e3bca8b8..36005186f 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -182,9 +182,9 @@ export default class StyleableModel ex return; } }); - const resolvedProps = this.dataResolverWatchers.addProps({ style: newStyle }, opts) as Partial; - this.set(resolvedProps, opts as any); - newStyle = resolvedProps['style']! as StyleProps; + + this.set({ style: newStyle }, opts); + newStyle = this.attributes['style'] as StyleProps; const changedKeys = Object.keys(shallowDiff(propOrig, propNew)); const diff: ObjectAny = changedKeys.reduce((acc, key) => { diff --git a/packages/core/src/editor/model/Editor.ts b/packages/core/src/editor/model/Editor.ts index 7a7dfcd51..1dc90fee5 100644 --- a/packages/core/src/editor/model/Editor.ts +++ b/packages/core/src/editor/model/Editor.ts @@ -464,12 +464,13 @@ export default class EditorModel extends Model { * @param {Object} opt Options * @private * */ - handleUpdates(model: any, val: any, opt: any = {}) { + handleUpdates(opt: any = {}, data: Record) { // Component has been added temporarily - do not update storage or record changes if (this.__skip || !this.loadTriggered || opt.temporary || opt.noCount || opt.avoidStore || opt.partial) { return; } + this.trigger(this.events.updateBefore, data); this.timedInterval && clearTimeout(this.timedInterval); this.timedInterval = setTimeout(() => { const curr = this.getDirtyCount() || 0; @@ -478,8 +479,8 @@ export default class EditorModel extends Model { }, 0); } - changesUp(opts: any) { - this.handleUpdates(0, 0, opts); + changesUp(opts: any, data: Record) { + this.handleUpdates(opts, data); } /** diff --git a/packages/core/src/editor/types.ts b/packages/core/src/editor/types.ts index be265e786..fda4e1a56 100644 --- a/packages/core/src/editor/types.ts +++ b/packages/core/src/editor/types.ts @@ -53,6 +53,7 @@ export enum EditorEvents { * editor.on('update', () => { ... }); */ update = 'update', + updateBefore = 'updateBefore', /** * @event `undo` Undo executed. diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7a5941c07..c7f532747 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -149,6 +149,8 @@ export type { default as DataRecord } from './data_sources/model/DataRecord'; export type { default as DataRecords } from './data_sources/model/DataRecords'; export type { default as DataVariable } from './data_sources/model/DataVariable'; export type { default as ComponentDataVariable } from './data_sources/model/ComponentDataVariable'; +export type { default as ComponentWithCollectionsState } from './data_sources/model/ComponentWithCollectionsState'; +export type { ComponentWithDataResolver } from './data_sources/model/ComponentWithDataResolver'; export type { default as ComponentDataCollection } from './data_sources/model/data_collection/ComponentDataCollection'; export type { default as ComponentDataCondition } from './data_sources/model/conditional_variables/ComponentDataCondition'; export type { diff --git a/packages/core/src/pages/index.ts b/packages/core/src/pages/index.ts index 7e278b6db..0005063c6 100644 --- a/packages/core/src/pages/index.ts +++ b/packages/core/src/pages/index.ts @@ -116,7 +116,10 @@ export default class PageManager extends ItemManagerModule em.changesUp(o || c)); + pages.on('add remove reset change', (page, c, o) => { + const options = o || c; + em.changesUp(o || c, { page, options }); + }); } /** diff --git a/packages/core/test/common.ts b/packages/core/test/common.ts index 385db4899..8c64a51e3 100644 --- a/packages/core/test/common.ts +++ b/packages/core/test/common.ts @@ -1,11 +1,7 @@ -import { DataSource } from '../src'; import CanvasEvents from '../src/canvas/types'; import { ObjectAny } from '../src/common'; -import { - DataConditionIfFalseType, - DataConditionIfTrueType, -} from '../src/data_sources/model/conditional_variables/constants'; import { NumberOperation } from '../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { DataComponentTypes } from '../src/data_sources/types'; import Editor from '../src/editor'; import { EditorConfig } from '../src/editor/config/config'; import EditorModel from '../src/editor/model/Editor'; @@ -61,6 +57,9 @@ export function setupTestEditor(opts?: { withCanvas?: boolean; config?: Partial< editor.DataSources.postLoad(); editor.Components.postLoad(); editor.Pages.postLoad(); + + em.set({ readyLoad: true, readyCanvas: true, ready: true }); + em.loadTriggered = true; } return { editor, em, dsm, um, cmpRoot, fixtures }; @@ -138,11 +137,12 @@ const createConditionalComponentDef = (type: string, content: string) => ({ components: [createContent(content)], }); +const DataConditionIfTrueType = DataComponentTypes.conditionTrue; +const DataConditionIfFalseType = DataComponentTypes.conditionFalse; export const ifTrueText = 'true text'; export const newIfTrueText = 'new true text'; export const ifFalseText = 'false text'; export const newIfFalseText = 'new false text'; - export const ifTrueComponentDef = createConditionalComponentDef(DataConditionIfTrueType, ifTrueText); export const newIfTrueComponentDef = createConditionalComponentDef(DataConditionIfTrueType, newIfTrueText); export const ifFalseComponentDef = createConditionalComponentDef(DataConditionIfFalseType, ifFalseText); diff --git a/packages/core/test/specs/data_sources/dynamic_values/attributes.ts b/packages/core/test/specs/data_sources/dynamic_values/attributes.ts index 61d1ae65d..feebb7b65 100644 --- a/packages/core/test/specs/data_sources/dynamic_values/attributes.ts +++ b/packages/core/test/specs/data_sources/dynamic_values/attributes.ts @@ -240,6 +240,63 @@ describe('Dynamic Attributes', () => { const input = cmp.getEl(); expect(input?.getAttribute('dynamicAttribute')).toBe(null); }); + + test('attributes should not collide with styles', () => { + ({ em, dsm, cmpRoot } = setupTestEditor({ config: { avoidInlineStyle: false } })); + dsm.add({ + id: 'ds_id', + records: [ + { id: 'id1', value: 'test-value' }, + { id: 'id2', value: 'second-test-value' }, + ], + }); + + const cmp = cmpRoot.append({ + tagName: 'input', + })[0]; + + cmp.addAttributes({ + static: 'static-value-attr', + dynamic: { + type: DataVariableType, + defaultValue: 'default', + path: 'ds_id.id1.value', + }, + }); + cmp.addStyle({ + static: 'static-value-style', + dynamic: { + type: DataVariableType, + defaultValue: 'default', + path: 'ds_id.id2.value', + }, + }); + + testAttribute(cmp, 'style', 'static:static-value-style;dynamic:second-test-value;'); + testAttribute(cmp, 'dynamic', 'test-value'); + testAttribute(cmp, 'static', 'static-value-attr'); + + cmp.addAttributes({ + static: 'static-value-attr-2', + dynamic: { + type: DataVariableType, + defaultValue: 'default', + path: 'ds_id.id2.value', + }, + }); + cmp.addStyle({ + static: 'static-value-style-2', + dynamic: { + type: DataVariableType, + defaultValue: 'default', + path: 'ds_id.id1.value', + }, + }); + + testAttribute(cmp, 'style', 'static:static-value-style-2;dynamic:test-value;'); + testAttribute(cmp, 'dynamic', 'second-test-value'); + testAttribute(cmp, 'static', 'static-value-attr-2'); + }); }); function changeDataSourceValue(dsm: DataSourceManager, id: string) { diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts index dc527d437..1339962d2 100644 --- a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentDataCondition.ts @@ -1,9 +1,10 @@ -import { Component, Components, ComponentView, DataSourceManager, Editor } from '../../../../../src'; -import { DataConditionIfTrueType } from '../../../../../src/data_sources/model/conditional_variables/constants'; +import { DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition'; import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { AnyTypeOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/AnyTypeOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { DataComponentTypes } from '../../../../../src/data_sources/types'; import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import EditorModel from '../../../../../src/editor/model/Editor'; @@ -17,7 +18,6 @@ import { setupTestEditor, TRUE_CONDITION, } from '../../../../common'; -import ComponentDataCondition from '../../../../../src/data_sources/model/conditional_variables/ComponentDataCondition'; describe('ComponentDataCondition', () => { let editor: Editor; @@ -147,7 +147,7 @@ describe('ComponentDataCondition', () => { }, components: [ { - type: DataConditionIfTrueType, + type: DataComponentTypes.conditionTrue, components: { type: DataConditionType, dataResolver: { diff --git a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts index a83193790..11a4480bc 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts +++ b/packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts @@ -1,16 +1,16 @@ import { Component, DataSource, DataSourceManager } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { - DataCollectionItemType, - DataCollectionType, -} from '../../../../../src/data_sources/model/data_collection/constants'; +import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection'; import { ComponentDataCollectionProps, DataCollectionStateType, } from '../../../../../src/data_sources/model/data_collection/types'; +import { DataComponentTypes } from '../../../../../src/data_sources/types'; import EditorModel from '../../../../../src/editor/model/Editor'; import { setupTestEditor } from '../../../../common'; -import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection'; + +const DataCollectionItemType = DataComponentTypes.collectionItem; +const DataCollectionType = DataComponentTypes.collection; describe('Collection component getters and setters', () => { let em: EditorModel; 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 41169940a..5c2b85934 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 @@ -1,17 +1,17 @@ import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { - DataCollectionItemType, - DataCollectionType, -} from '../../../../../src/data_sources/model/data_collection/constants'; +import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection'; import { ComponentDataCollectionProps, DataCollectionStateType, } from '../../../../../src/data_sources/model/data_collection/types'; +import { DataComponentTypes } from '../../../../../src/data_sources/types'; import EditorModel from '../../../../../src/editor/model/Editor'; -import { setupTestEditor } from '../../../../common'; import { ProjectData } from '../../../../../src/storage_manager'; -import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection'; +import { setupTestEditor } from '../../../../common'; + +const DataCollectionItemType = DataComponentTypes.collectionItem; +const DataCollectionType = DataComponentTypes.collection; describe('Collection component', () => { let em: EditorModel; @@ -104,6 +104,12 @@ describe('Collection component', () => { expect(cmp.getInnerHTML()).toBe(innerHTML); expect(cmp.toHTML()).toBe(`<${tagName} id="${cmp.getId()}">${innerHTML}`); expect(cmp.getEl()?.innerHTML).toBe(innerHTML); + expect(JSON.parse(JSON.stringify(cmp.toJSON()))).toEqual({ + tagName: cmp.tagName, + dataResolver: cmp.get('dataResolver'), + type: cmp.getType(), + attributes: cmp.getAttributes(), + }); }; const checkRecordsWithInnerCmp = () => { @@ -621,13 +627,34 @@ describe('Collection component', () => { const collectionCmpDef = { type: DataCollectionType, + attributes: { id: 'cmp-coll' }, components: [ { type: DataCollectionItemType, + attributes: { id: 'cmp-coll-item' }, components: [ { ...childCmpDef, - components: [childCmpDef, childCmpDef], + attributes: { + ...childCmpDef.attributes, + id: 'cmp-coll-item-child-1', + }, + components: [ + { + ...childCmpDef, + attributes: { + ...childCmpDef.attributes, + id: 'cmp-coll-item-child-1-1', + }, + }, + { + ...childCmpDef, + attributes: { + ...childCmpDef.attributes, + id: 'cmp-coll-item-child-1-2', + }, + }, + ], }, ], }, @@ -652,6 +679,9 @@ describe('Collection component', () => { const firstItemCmp = cmp.getCollectionItemComponents().at(0); const newChildDefinition = { type: 'default', + attributes: { + id: 'cmp-coll-item-child-UP', + }, name: { type: DataVariableType, variableType: DataCollectionStateType.currentIndex, @@ -674,6 +704,9 @@ describe('Collection component', () => { const firstItemCmp = cmp.getCollectionItemComponents().at(0); const newChildDefinition = { type: 'default', + attributes: { + id: 'cmp-coll-item-child-UP', + }, name: { type: DataVariableType, variableType: DataCollectionStateType.currentIndex, 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 764667c76..6e9efe0d8 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 @@ -1,17 +1,17 @@ import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; -import { - DataCollectionItemType, - DataCollectionType, -} from '../../../../../src/data_sources/model/data_collection/constants'; import { ComponentDataCollectionProps, DataCollectionStateType, } from '../../../../../src/data_sources/model/data_collection/types'; +import { DataComponentTypes } from '../../../../../src/data_sources/types'; import EditorModel from '../../../../../src/editor/model/Editor'; import { ProjectData } from '../../../../../src/storage_manager'; import { setupTestEditor } from '../../../../common'; +const DataCollectionItemType = DataComponentTypes.collectionItem; +const DataCollectionType = DataComponentTypes.collection; + describe('Collection variable components', () => { let em: EditorModel; let editor: Editor; @@ -116,6 +116,7 @@ describe('Collection variable components', () => { beforeEach(() => { const variableCmpDef = { type: DataVariableType, + attributes: { id: 'cmp-coll-item-child' }, dataResolver: { variableType: DataCollectionStateType.currentItem, collectionId: 'my_collection', @@ -125,11 +126,14 @@ describe('Collection variable components', () => { const collectionCmpDef = { type: DataCollectionType, + attributes: { id: 'cmp-coll' }, components: { type: DataCollectionItemType, + attributes: { id: 'cmp-coll-item' }, components: [ { type: 'default', + attributes: { id: 'cmp-coll-item-child-1' }, }, variableCmpDef, ], @@ -154,6 +158,7 @@ describe('Collection variable components', () => { const firstChild = cmp.components().at(0); const newChildDefinition = { type: DataVariableType, + attributes: { id: 'cmp-var' }, dataResolver: { variableType: DataCollectionStateType.currentIndex, collectionId: 'my_collection', @@ -175,6 +180,7 @@ describe('Collection variable components', () => { const firstChild = cmp.components().at(0); const newChildDefinition = { type: DataVariableType, + attributes: { id: 'cmp-var' }, dataResolver: { variableType: DataCollectionStateType.currentIndex, collectionId: 'my_collection', diff --git a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap index 6af45f11c..ddc58494c 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap +++ b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap @@ -2,8 +2,14 @@ exports[`Collection component Serialization Saving: Collection with grandchildren 1`] = ` { + "attributes": { + "id": "cmp-coll", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item", + }, "components": [ { "attributes": { @@ -13,6 +19,7 @@ exports[`Collection component Serialization Saving: Collection with grandchildre "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1", "name": { "collectionId": "my_collection", "path": "user", @@ -22,6 +29,9 @@ exports[`Collection component Serialization Saving: Collection with grandchildre }, "components": [ { + "attributes": { + "id": "cmp-coll-item-child-UP", + }, "name": { "collectionId": "my_collection", "path": "user", @@ -70,8 +80,14 @@ exports[`Collection component Serialization Saving: Collection with grandchildre exports[`Collection component Serialization Saving: Collection with no grandchildren 1`] = ` { + "attributes": { + "id": "cmp-coll", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item", + }, "components": [ { "attributes": { @@ -81,6 +97,7 @@ exports[`Collection component Serialization Saving: Collection with no grandchil "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1", "name": { "collectionId": "my_collection", "path": "user", @@ -97,6 +114,7 @@ exports[`Collection component Serialization Saving: Collection with no grandchil "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1-1", "name": { "collectionId": "my_collection", "path": "user", @@ -132,6 +150,7 @@ exports[`Collection component Serialization Saving: Collection with no grandchil "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1-2", "name": { "collectionId": "my_collection", "path": "user", @@ -199,8 +218,14 @@ exports[`Collection component Serialization Saving: Collection with no grandchil exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with grandchildren 1`] = ` { + "attributes": { + "id": "cmp-coll", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item", + }, "components": [ { "attributes": { @@ -210,6 +235,7 @@ exports[`Collection component Serialization Serializion with Collection Variable "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1", "name": { "collectionId": "my_collection", "path": "user", @@ -226,6 +252,7 @@ exports[`Collection component Serialization Serializion with Collection Variable "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1-1", "name": { "collectionId": "my_collection", "path": "user", @@ -235,6 +262,9 @@ exports[`Collection component Serialization Serializion with Collection Variable }, "components": [ { + "attributes": { + "id": "cmp-coll-item-child-UP", + }, "name": { "collectionId": "my_collection", "path": "user", @@ -272,6 +302,7 @@ exports[`Collection component Serialization Serializion with Collection Variable "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1-2", "name": { "collectionId": "my_collection", "path": "user", @@ -339,8 +370,14 @@ exports[`Collection component Serialization Serializion with Collection Variable exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with no grandchildren 1`] = ` { + "attributes": { + "id": "cmp-coll", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item", + }, "components": [ { "attributes": { @@ -350,6 +387,7 @@ exports[`Collection component Serialization Serializion with Collection Variable "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1", "name": { "collectionId": "my_collection", "path": "user", @@ -366,6 +404,7 @@ exports[`Collection component Serialization Serializion with Collection Variable "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1-1", "name": { "collectionId": "my_collection", "path": "user", @@ -401,6 +440,7 @@ exports[`Collection component Serialization Serializion with Collection Variable "type": "data-variable", "variableType": "currentItem", }, + "id": "cmp-coll-item-child-1-2", "name": { "collectionId": "my_collection", "path": "user", diff --git a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap index 27dc4a319..1c7c6c45f 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap +++ b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionWithDataVariable.ts.snap @@ -2,13 +2,25 @@ exports[`Collection variable components Serialization Saving: Collection with collection variable component ( no grandchildren ) 1`] = ` { + "attributes": { + "id": "cmp-coll", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item-child-1", + }, "type": "default", }, { + "attributes": { + "id": "cmp-coll-item-child", + }, "dataResolver": { "collectionId": "my_collection", "path": "user", @@ -35,12 +47,24 @@ exports[`Collection variable components Serialization Saving: Collection with co exports[`Collection variable components Serialization Saving: Collection with collection variable component ( with grandchildren ) 1`] = ` { + "attributes": { + "id": "cmp-coll", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item-child-1", + }, "components": [ { + "attributes": { + "id": "cmp-var", + }, "dataResolver": { "collectionId": "my_collection", "path": "user", @@ -52,6 +76,9 @@ exports[`Collection variable components Serialization Saving: Collection with co "type": "default", }, { + "attributes": { + "id": "cmp-coll-item-child", + }, "dataResolver": { "collectionId": "my_collection", "path": "user", @@ -78,13 +105,25 @@ exports[`Collection variable components Serialization Saving: Collection with co exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( no grandchildren ) 1`] = ` { + "attributes": { + "id": "cmp-coll", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item-child-1", + }, "type": "default", }, { + "attributes": { + "id": "cmp-coll-item-child", + }, "dataResolver": { "collectionId": "my_collection", "path": "user", @@ -111,12 +150,24 @@ exports[`Collection variable components Serialization Serializion to JSON: Colle exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( with grandchildren ) 1`] = ` { + "attributes": { + "id": "cmp-coll", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item-child-1", + }, "components": [ { + "attributes": { + "id": "cmp-var", + }, "dataResolver": { "collectionId": "my_collection", "path": "user", @@ -128,6 +179,9 @@ exports[`Collection variable components Serialization Serializion to JSON: Colle "type": "default", }, { + "attributes": { + "id": "cmp-coll-item-child", + }, "dataResolver": { "collectionId": "my_collection", "path": "user", diff --git a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap index b88dfe422..4fec6cb6a 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap +++ b/packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap @@ -2,14 +2,29 @@ exports[`Collection component Nested collections are correctly serialized 1`] = ` { + "attributes": { + "id": "cmp-coll-parent", + }, "components": [ { + "attributes": { + "id": "cmp-coll-parent-item", + }, "components": [ { + "attributes": { + "id": "cmp-coll", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item", + }, "components": [ { + "attributes": { + "id": "cmp-coll-item-child-1", + }, "name": { "collectionId": "nested_collection", "path": "user", diff --git a/packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts b/packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts index 8bc39e5f4..7775947b7 100644 --- a/packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts +++ b/packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts @@ -1,17 +1,17 @@ -import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src'; +import { Component, DataRecord, DataSource, DataSourceManager } from '../../../../../src'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection'; -import { - DataCollectionItemType, - DataCollectionType, -} from '../../../../../src/data_sources/model/data_collection/constants'; import { ComponentDataCollectionProps, DataCollectionStateType, } from '../../../../../src/data_sources/model/data_collection/types'; +import { DataComponentTypes } from '../../../../../src/data_sources/types'; import EditorModel from '../../../../../src/editor/model/Editor'; import { setupTestEditor } from '../../../../common'; +const DataCollectionItemType = DataComponentTypes.collectionItem; +const DataCollectionType = DataComponentTypes.collection; + describe('Collection component', () => { let em: EditorModel; let dsm: DataSourceManager; @@ -28,7 +28,12 @@ describe('Collection component', () => { function getCmpDef(nestedCmpDef: ComponentDataCollectionProps): ComponentDataCollectionProps { return { type: DataCollectionType, - components: { type: DataCollectionItemType, components: nestedCmpDef }, + attributes: { id: 'cmp-coll-parent' }, + components: { + type: DataCollectionItemType, + attributes: { id: 'cmp-coll-parent-item' }, + components: nestedCmpDef, + }, dataResolver: { collectionId: 'parent_collection', dataSource: { @@ -64,10 +69,13 @@ describe('Collection component', () => { nestedCmpDef = { type: DataCollectionType, + attributes: { id: 'cmp-coll' }, components: { type: DataCollectionItemType, + attributes: { id: 'cmp-coll-item' }, components: { type: 'default', + attributes: { id: 'cmp-coll-item-child-1' }, name: { type: DataVariableType, variableType: DataCollectionStateType.currentItem, diff --git a/packages/core/test/specs/dom_components/model/Component.ts b/packages/core/test/specs/dom_components/model/Component.ts index 8f8aa2ce0..3fee7487c 100644 --- a/packages/core/test/specs/dom_components/model/Component.ts +++ b/packages/core/test/specs/dom_components/model/Component.ts @@ -460,6 +460,30 @@ describe('Component', () => { ); }); + test('Ensure duplicated component clones the rules also cross components', () => { + const idName = 'test'; + const cmp = dcomp.addComponent(` +
+
Comp 1
+
+ + `) as Component; + const cmp2 = dcomp.addComponent(`
Text
`) as Component; + cmp2.components().resetFromString(`
Comp 2
`); + const newId = cmp2.components().at(0).getId(); + expect(em.getHtml()).toBe( + `
Comp 1
Comp 2
`, + ); + expect(em.getCss()).toBe( + `#test{color:red;}#${newId}{color:red;}@media (max-width: 992px){#test{color:blue;}#${newId}{color:blue;}}`, + ); + }); + test('Ability to stop/change propagation chain', () => { obj.append({ removable: false, diff --git a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts index 384650694..ae7416bb6 100644 --- a/packages/core/test/specs/dom_components/model/ComponentWrapper.ts +++ b/packages/core/test/specs/dom_components/model/ComponentWrapper.ts @@ -1,9 +1,10 @@ -import { DataSourceManager, DataSource, DataRecord } from '../../../../src'; +import { DataRecord, DataSourceManager } from '../../../../src'; import { DataVariableProps, DataVariableType } from '../../../../src/data_sources/model/DataVariable'; +import { DataComponentTypes } from '../../../../src/data_sources/types'; +import { keyRootData } from '../../../../src/dom_components/constants'; 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 { keyRootData } from '../../../../src/dom_components/constants'; import Editor from '../../../../src/editor'; import EditorModel from '../../../../src/editor/model/Editor'; import { setupTestEditor } from '../../../common'; @@ -43,10 +44,11 @@ describe('ComponentWrapper', () => { describe('ComponentWrapper with DataResolver', () => { let em: EditorModel; let dsm: DataSourceManager; - let blogDataSource: DataSource; let wrapper: ComponentWrapper; let firstRecord: DataRecord; + const contentDataSourceId = 'contentDataSource'; + const blogDataSourceId = 'blogs'; const firstBlog = { id: 'blog1', title: 'How to Test Components' }; const blogsData = [ firstBlog, @@ -60,11 +62,11 @@ describe('ComponentWrapper', () => { }; beforeEach(() => { - ({ em, dsm } = setupTestEditor()); + ({ em, dsm } = setupTestEditor({ withCanvas: true })); wrapper = em.getWrapper() as ComponentWrapper; - blogDataSource = dsm.add({ - id: 'contentDataSource', + dsm.add({ + id: contentDataSourceId, records: [ { id: 'blogs', @@ -77,7 +79,12 @@ describe('ComponentWrapper', () => { ], }); - firstRecord = em.DataSources.get('contentDataSource').getRecord('blogs')!; + dsm.add({ + id: blogDataSourceId, + records: blogsData, + }); + + firstRecord = em.DataSources.get(contentDataSourceId).getRecord('blogs')!; }); afterEach(() => { @@ -93,15 +100,20 @@ describe('ComponentWrapper', () => { wrapper.append({ type: 'default', title: { - type: 'data-variable', + type: DataComponentTypes.variable, collectionId: keyRootData, path, }, + components: { + tagName: 'span', + type: DataComponentTypes.variable, + dataResolver: { collectionId: keyRootData, path }, + }, })[0]; test('children reflect resolved value from dataResolver', () => { wrapper.setDataResolver(createDataResolver('contentDataSource.blogs.data')); - wrapper.resolverCurrentItem = 0; + wrapper.setResolverCurrentItem(0); const child = appendChildWithTitle(); expect(child.get('title')).toBe(blogsData[0].title); @@ -113,7 +125,7 @@ describe('ComponentWrapper', () => { test('children update collectionStateMap on wrapper.setDataResolver', () => { const child = appendChildWithTitle(); wrapper.setDataResolver(createDataResolver('contentDataSource.blogs.data')); - wrapper.resolverCurrentItem = 0; + wrapper.setResolverCurrentItem(0); expect(child.get('title')).toBe(blogsData[0].title); @@ -123,10 +135,32 @@ describe('ComponentWrapper', () => { test('wrapper should handle objects as collection state', () => { wrapper.setDataResolver(createDataResolver('contentDataSource.productsById.data')); - wrapper.resolverCurrentItem = 'product1'; + wrapper.setResolverCurrentItem('product1'); const child = appendChildWithTitle('title'); expect(child.get('title')).toBe(productsById.product1.title); }); + + test('wrapper should handle default data source records', () => { + wrapper.setDataResolver(createDataResolver(blogDataSourceId)); + + const child = appendChildWithTitle('title'); + expect(child.get('title')).toBe(blogsData[0].title); + expect(child.getInnerHTML()).toBe(`${blogsData[0].title}`); + + const eventUpdate = jest.fn(); + em.on(em.events.updateBefore, eventUpdate); + + wrapper.setResolverCurrentItem(1); + expect(child.get('title')).toBe(blogsData[1].title); + expect(child.getInnerHTML()).toBe(`${blogsData[1].title}`); + + wrapper.setResolverCurrentItem(blogsData[2].id); + expect(child.get('title')).toBe(blogsData[2].title); + expect(child.getInnerHTML()).toBe(`${blogsData[2].title}`); + + // No update events are expected + expect(eventUpdate).toHaveBeenCalledTimes(0); + }); }); }); From 61ebd6c50ab2f629517077d30de2623994ca0654 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Mon, 17 Nov 2025 14:08:24 +0400 Subject: [PATCH 2/4] Release GrapesJS core rc: v0.22.14-rc.3 (#6650) --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 22b48d070..470c1f245 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Free and Open Source Web Builder Framework", - "version": "0.22.14-rc.2", + "version": "0.22.14-rc.3", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com", From f59e981f76ff378f59c06cf706af88e502db098e Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Wed, 19 Nov 2025 10:28:38 +0400 Subject: [PATCH 3/4] Align component wrapper resolver with collections (#6653) --- packages/core/src/data_sources/index.ts | 9 +- .../src/data_sources/model/DataVariable.ts | 22 ++--- packages/core/src/data_sources/types.ts | 4 + packages/core/src/dom_components/constants.ts | 1 - .../src/dom_components/model/Component.ts | 88 +++++++++---------- .../dom_components/model/ComponentWrapper.ts | 10 ++- .../dom_components/model/ComponentWrapper.ts | 8 +- 7 files changed, 71 insertions(+), 71 deletions(-) delete mode 100644 packages/core/src/dom_components/constants.ts diff --git a/packages/core/src/data_sources/index.ts b/packages/core/src/data_sources/index.ts index 09560ffe8..fb737164d 100644 --- a/packages/core/src/data_sources/index.ts +++ b/packages/core/src/data_sources/index.ts @@ -36,12 +36,13 @@ import { DataCollectionStateType } from './model/data_collection/types'; import DataRecord from './model/DataRecord'; import DataSource from './model/DataSource'; import DataSources from './model/DataSources'; -import { DataComponentTypes, DataRecordProps, DataSourceProps, DataSourcesEvents } from './types'; +import { DataCollectionKeys, DataComponentTypes, DataRecordProps, DataSourceProps, DataSourcesEvents } from './types'; export default class DataSourceManager extends ItemManagerModule { storageKey = 'dataSources'; events = DataSourcesEvents; dataComponentTypes = DataComponentTypes; + dataCollectionKeys = DataCollectionKeys; dataCollectionStateTypes = DataCollectionStateType; dataOperationTypes = { any: AnyTypeOperation, @@ -104,8 +105,8 @@ export default class DataSourceManager extends ItemManagerModule }) { + return get(opts?.context || this.getContext(), path, defValue); } /** @@ -130,7 +131,7 @@ export default class DataSourceManager extends ItemManagerModule { acc[ds.id] = ds.records.reduce((accR, dr, i) => { const dataRecord = dr; diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index b14e8f63a..356247e98 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -1,14 +1,8 @@ import { Model } from '../../common'; -import { keyRootData } from '../../dom_components/constants'; import EditorModel from '../../editor/model/Editor'; import { DataComponentTypes } from '../types'; import { isDataVariable } from '../utils'; -import { - DataCollectionStateMap, - DataCollectionState, - DataCollectionStateType, - RootDataType, -} from './data_collection/types'; +import { DataCollectionState, DataCollectionStateMap, DataCollectionStateType } from './data_collection/types'; export const DataVariableType = DataComponentTypes.variable as const; @@ -163,18 +157,15 @@ export default class DataVariable extends Model { 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; } if (variableType === 'currentItem') { - return DataVariable.resolveCurrentItem(collectionItem as DataCollectionState, path, collectionId, em); + return ( + DataVariable.resolveCurrentItem(collectionItem as DataCollectionState, path, collectionId, em) ?? defaultValue + ); } const state = collectionItem as DataCollectionState; @@ -190,7 +181,7 @@ export default class DataVariable extends Model { const currentItem = collectionItem.currentItem; if (!currentItem) { em.logError(`Current item is missing for collection: ${collectionId}`); - return ''; + return; } if (currentItem.type === DataVariableType) { @@ -199,8 +190,7 @@ export default class DataVariable extends Model { } if (path && !(currentItem as any)[path]) { - em.logError(`Path not found in current item: ${path} for collection: ${collectionId}`); - return ''; + return; } return path ? (currentItem as any)[path] : currentItem; diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index 5ddc65bf4..3fd251c90 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -22,6 +22,10 @@ export enum DataComponentTypes { collectionItem = 'data-collection-item', } +export enum DataCollectionKeys { + rootData = '__rootData', +} + export interface DataRecordProps extends ObjectAny { /** * Record id. diff --git a/packages/core/src/dom_components/constants.ts b/packages/core/src/dom_components/constants.ts deleted file mode 100644 index 6b5aa1bbc..000000000 --- a/packages/core/src/dom_components/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const keyRootData = '__rootData'; diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 345dcff3f..58c689047 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -1,60 +1,53 @@ +import { Model, ModelDestroyOptions } from 'backbone'; import { - isUndefined, - isFunction, + bindAll, + forEach, + has, isArray, - isEmpty, isBoolean, - has, + isEmpty, + isFunction, isString, - forEach, - result, - bindAll, + isUndefined, keys, + result, } from 'underscore'; -import { - shallowDiff, - capitalize, - isEmptyObj, - isObject, - toLowerCase, - escapeAltQuoteAttrValue, - escapeAttrValue, -} from '../../utils/mixins'; +import Frame from '../../canvas/model/Frame'; +import { AddOptions, ExtractMethods, ObjectAny, PrevToNewIdMap, SetOptions } from '../../common'; +import CssRule, { CssRuleJSON } from '../../css_composer/model/CssRule'; +import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; +import { DataCollectionKeys } from '../../data_sources/types'; +import { checkAndGetSyncableCollectionItemId } from '../../data_sources/utils'; import StyleableModel, { GetStyleOpts, StyleProps, UpdateStyleOptions, } from '../../domain_abstract/model/StyleableModel'; -import { Model, ModelDestroyOptions } from 'backbone'; -import Components from './Components'; +import EditorModel from '../../editor/model/Editor'; +import ItemView from '../../navigator/view/ItemView'; import Selector from '../../selector_manager/model/Selector'; import Selectors from '../../selector_manager/model/Selectors'; +import Trait from '../../trait_manager/model/Trait'; import Traits from '../../trait_manager/model/Traits'; -import EditorModel from '../../editor/model/Editor'; +import { TraitProperties } from '../../trait_manager/types'; import { - ComponentAdd, - ComponentDefinition, - ComponentDefinitionDefined, - ComponentOptions, - ComponentProperties, - DragMode, - ResetComponentsOptions, - SymbolToUpOptions, - ToHTMLOptions, -} from './types'; -import Frame from '../../canvas/model/Frame'; + capitalize, + escapeAltQuoteAttrValue, + escapeAttrValue, + isEmptyObj, + isObject, + shallowDiff, + toLowerCase, +} from '../../utils/mixins'; import { DomComponentsConfig } from '../config/config'; -import ComponentView from '../view/ComponentView'; -import { AddOptions, ExtractMethods, ObjectAny, PrevToNewIdMap, SetOptions } from '../../common'; -import CssRule, { CssRuleJSON } from '../../css_composer/model/CssRule'; -import Trait from '../../trait_manager/model/Trait'; -import { ToolbarButtonProps } from './ToolbarButton'; -import { TraitProperties } from '../../trait_manager/types'; import { ActionLabelComponents, ComponentsEvents } from '../types'; -import ItemView from '../../navigator/view/ItemView'; +import ComponentView from '../view/ComponentView'; +import Components from './Components'; +import { DataWatchersOptions } from './ModelResolverWatcher'; import { - getSymbolMain, getSymbolInstances, + getSymbolMain, + getSymbolsToUpdate, initSymbol, isSymbol, isSymbolMain, @@ -62,12 +55,19 @@ import { updateSymbolCls, updateSymbolComps, updateSymbolProps, - getSymbolsToUpdate, } from './SymbolUtils'; -import { DataWatchersOptions } from './ModelResolverWatcher'; -import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; -import { checkAndGetSyncableCollectionItemId } from '../../data_sources/utils'; -import { keyRootData } from '../constants'; +import { ToolbarButtonProps } from './ToolbarButton'; +import { + ComponentAdd, + ComponentDefinition, + ComponentDefinitionDefined, + ComponentOptions, + ComponentProperties, + DragMode, + ResetComponentsOptions, + SymbolToUpOptions, + ToHTMLOptions, +} from './types'; export interface IComponent extends ExtractMethods {} export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DataWatchersOptions {} @@ -446,7 +446,7 @@ export default class Component extends StyleableModel { this.emitWithEditor(ComponentsEvents.styleUpdate, this, pros); styleKeys.forEach((key) => this.emitWithEditor(`${ComponentsEvents.styleUpdateProperty}${key}`, this, pros)); - const parentCollectionIds = Object.keys(collectionsStateMap).filter((key) => key !== keyRootData); + const parentCollectionIds = Object.keys(collectionsStateMap).filter((key) => key !== DataCollectionKeys.rootData); if (parentCollectionIds.length === 0) return; diff --git a/packages/core/src/dom_components/model/ComponentWrapper.ts b/packages/core/src/dom_components/model/ComponentWrapper.ts index cb6271444..f8ff2f97e 100644 --- a/packages/core/src/dom_components/model/ComponentWrapper.ts +++ b/packages/core/src/dom_components/model/ComponentWrapper.ts @@ -3,8 +3,8 @@ import ComponentWithCollectionsState from '../../data_sources/model/ComponentWit import DataResolverListener from '../../data_sources/model/DataResolverListener'; import { DataVariableProps } from '../../data_sources/model/DataVariable'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; +import { DataCollectionKeys } from '../../data_sources/types'; import { attrToString } from '../../utils/dom'; -import { keyRootData } from '../constants'; import Component from './Component'; import ComponentHead, { type as typeHead } from './ComponentHead'; import Components from './Components'; @@ -160,15 +160,19 @@ export default class ComponentWrapper extends ComponentWithCollectionsState { + const keyRootData = DataCollectionKeys.rootData; let em: Editor; beforeEach(() => { @@ -101,13 +102,14 @@ describe('ComponentWrapper', () => { type: 'default', title: { type: DataComponentTypes.variable, + variableType: DataCollectionStateType.currentItem, collectionId: keyRootData, path, }, components: { tagName: 'span', type: DataComponentTypes.variable, - dataResolver: { collectionId: keyRootData, path }, + dataResolver: { collectionId: keyRootData, variableType: DataCollectionStateType.currentItem, path }, }, })[0]; From fdb55d6111dab219a74aad985c609597f49ab2e9 Mon Sep 17 00:00:00 2001 From: Artur Arseniev Date: Thu, 20 Nov 2025 17:17:59 +0400 Subject: [PATCH 4/4] Release GrapesJS core latest: v0.22.14 (#6656) --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 470c1f245..838879f5d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "grapesjs", "description": "Free and Open Source Web Builder Framework", - "version": "0.22.14-rc.3", + "version": "0.22.14", "author": "Artur Arseniev", "license": "BSD-3-Clause", "homepage": "http://grapesjs.com",