diff --git a/packages/core/src/css_composer/index.ts b/packages/core/src/css_composer/index.ts index 47f9c1719..32b158179 100644 --- a/packages/core/src/css_composer/index.ts +++ b/packages/core/src/css_composer/index.ts @@ -262,7 +262,7 @@ export default class CssComposer extends ItemManagerModule { } constructor(props: CssRuleProperties, opt: any = {}) { - super(props); + super(props, { em: opt.em }); this.config = props || {}; this.opt = opt; this.em = opt.em; @@ -326,7 +326,7 @@ export default class CssRule extends StyleableModel { if (isEmpty(obj.style)) delete obj.style; } - return obj; + return { ...obj, style: this.dataResolverWatchers.getStylesDefsOrValues(obj.style) }; } /** 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 0435b76f2..18895e11e 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -16,7 +16,7 @@ import { DataCollectionStateMap, } from './types'; import { detachSymbolInstance, getSymbolInstances } from '../../../dom_components/model/SymbolUtils'; -import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers'; +import { updateFromWatcher } from '../../../dom_components/model/ModelDataResolverWatchers'; import { ModelDestroyOptions } from 'backbone'; import Components from '../../../dom_components/model/Components'; diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 0903b5af3..437918e44 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -52,8 +52,8 @@ import { updateSymbolProps, getSymbolsToUpdate, } from './SymbolUtils'; -import { ComponentDataResolverWatchers } from './ComponentDataResolverWatchers'; -import { DynamicWatchersOptions } from './ComponentResolverWatcher'; +import { ModelDataResolverWatchers } from './ModelDataResolverWatchers'; +import { DynamicWatchersOptions } from './ModelResolverWatcher'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import { checkAndGetSyncableCollectionItemId } from '../../data_sources/utils'; @@ -264,18 +264,10 @@ export default class Component extends StyleableModel { * @private * @ts-ignore */ collection!: Components; - dataResolverWatchers: ComponentDataResolverWatchers; - collectionsStateMap: DataCollectionStateMap = {}; constructor(props: ComponentProperties = {}, opt: ComponentOptions) { const em = opt.em; - const dataResolverWatchers = new ComponentDataResolverWatchers(undefined, { em }); - super(props, { - ...opt, - dataResolverWatchers, - } as any); - dataResolverWatchers.bindComponent(this); - this.dataResolverWatchers = dataResolverWatchers; + super(props, opt); bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps', 'syncOnComponentChange'); @@ -344,6 +336,10 @@ export default class Component extends StyleableModel { isSymbol(this) && initSymbol(this); em?.trigger(ComponentsEvents.create, this, opt); } + + if (avoidInline(em)) { + this.dataResolverWatchers.disableStyles(); + } } set( @@ -353,7 +349,7 @@ export default class Component extends StyleableModel { ): this { let attributes: Partial; let options: ComponentSetOptions & { - dataResolverWatchers?: ComponentDataResolverWatchers; + dataResolverWatchers?: ModelDataResolverWatchers; } = { skipWatcherUpdates: false, fromDataSource: false }; if (typeof keyOrAttributes === 'object') { attributes = keyOrAttributes; @@ -368,7 +364,6 @@ export default class Component extends StyleableModel { this.dataResolverWatchers = this.dataResolverWatchers || options.dataResolverWatchers; const evaluatedProps = this.dataResolverWatchers.addProps(attributes, options); - return super.set(evaluatedProps, options); } @@ -812,8 +807,10 @@ export default class Component extends StyleableModel { */ getStyle(options: any = {}, optsAdd: any = {}) { const { em } = this; - const prop = isString(options) ? options : ''; - const opts = prop ? optsAdd : options; + const isOptionsString = isString(options); + const prop = isOptionsString ? options : ''; + const opts = isOptionsString || options === '' ? optsAdd : options; + const skipResolve = !!opts?.skipResolve; if (avoidInline(em) && !opts.inline) { const state = em.get('state'); @@ -822,15 +819,15 @@ export default class Component extends StyleableModel { this.rule = rule; if (rule) { - return rule.getStyle(prop); + return rule.getStyle(prop, { skipResolve }); } - // Return empty style if not rule have been found. We cannot return inline style with the next return + // Return empty style if no rule have been found. We cannot return inline style with the next return // because else on load inline style is set a #id or .class style return {}; } - return super.getStyle.call(this, prop); + return super.getStyle.call(this, prop, { skipResolve }); } /** @@ -849,7 +846,7 @@ export default class Component extends StyleableModel { prop = { ...prop, ...(style as any) }; const state = em.get('state'); const cc = em.Css; - const propOrig = this.getStyle(opts); + const propOrig = this.getStyle({ ...opts, skipResolve: true }); const newStyle = { ...propOrig, ...prop }; this.rule = cc.setIdRule(this.getId(), newStyle, { state, ...opts }); const diff = shallowDiff(propOrig, prop); diff --git a/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts b/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts similarity index 51% rename from packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts rename to packages/core/src/dom_components/model/ModelDataResolverWatchers.ts index 021984efa..1fb432824 100644 --- a/packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts +++ b/packages/core/src/dom_components/model/ModelDataResolverWatchers.ts @@ -1,38 +1,45 @@ import { ObjectAny } from '../../common'; -import Component from './Component'; +import StyleableModel from '../../domain_abstract/model/StyleableModel'; import { - ComponentResolverWatcher, - ComponentResolverWatcherOptions, + ModelResolverWatcher as ModelResolverWatcher, + ModelResolverWatcherOptions, DynamicWatchersOptions, -} from './ComponentResolverWatcher'; +} from './ModelResolverWatcher'; import { getSymbolsToUpdate } from './SymbolUtils'; export const updateFromWatcher = { fromDataSource: true, avoidStore: true }; -export class ComponentDataResolverWatchers { - private propertyWatcher: ComponentResolverWatcher; - private attributeWatcher: ComponentResolverWatcher; +export class ModelDataResolverWatchers { + private propertyWatcher: ModelResolverWatcher; + private attributeWatcher: ModelResolverWatcher; + private styleWatcher: ModelResolverWatcher; constructor( - private component: Component | undefined, - options: ComponentResolverWatcherOptions, + private model: StyleableModel | undefined, + options: ModelResolverWatcherOptions, ) { - this.propertyWatcher = new ComponentResolverWatcher(component, this.onPropertyUpdate, options); - this.attributeWatcher = new ComponentResolverWatcher(component, this.onAttributeUpdate, options); + this.propertyWatcher = new ModelResolverWatcher(model, this.onPropertyUpdate, options); + this.attributeWatcher = new ModelResolverWatcher(model, this.onAttributeUpdate, options); + this.styleWatcher = new ModelResolverWatcher(model, this.onStyleUpdate, options); } - private onPropertyUpdate(component: Component | undefined, key: string, value: any) { + private onPropertyUpdate(component: StyleableModel | undefined, key: string, value: any) { component?.set(key, value, updateFromWatcher); } - private onAttributeUpdate(component: Component | undefined, key: string, value: any) { - component?.addAttributes({ [key]: value }, updateFromWatcher); + private onAttributeUpdate(component: StyleableModel | undefined, key: string, value: any) { + (component as any)?.addAttributes({ [key]: value }, updateFromWatcher); } - bindComponent(component: Component) { - this.component = component; - this.propertyWatcher.bindComponent(component); - this.attributeWatcher.bindComponent(component); + private onStyleUpdate(component: StyleableModel | undefined, key: string, value: any) { + component?.addStyle({ [key]: value }, { ...updateFromWatcher, noEvent: true, partial: true, avoidStore: true }); + } + + bindModel(model: StyleableModel) { + this.model = model; + this.propertyWatcher.bindModel(model); + this.attributeWatcher.bindModel(model); + this.styleWatcher.bindModel(model); this.updateSymbolOverride(); } @@ -60,14 +67,27 @@ export class ComponentDataResolverWatchers { return evaluatedProps; } + setStyles(styles: ObjectAny, options: DynamicWatchersOptions = {}) { + return this.styleWatcher.setDynamicValues(styles, options); + } + + /** + * Disables inline style management for the component. Style handling is shifted to CSS rules + */ + disableStyles() { + this.styleWatcher.removeListeners(); + this.styleWatcher.destroy(); + } + removeAttributes(attributes: string[]) { this.attributeWatcher.removeListeners(attributes); this.updateSymbolOverride(); } private updateSymbolOverride() { - const isCollectionItem = !!Object.keys(this.component?.collectionsStateMap ?? {}).length; - if (!this.component || !isCollectionItem) return; + const model = this.model as any; + const isCollectionItem = !!Object.keys(model?.collectionsStateMap ?? {}).length; + if (!this.model || !isCollectionItem) return; const keys = this.propertyWatcher.getValuesResolvingFromCollections(); const attributesKeys = this.attributeWatcher.getValuesResolvingFromCollections(); @@ -76,16 +96,17 @@ export class ComponentDataResolverWatchers { const haveOverridenAttributes = Object.keys(attributesKeys).length; if (haveOverridenAttributes) combinedKeys.push('attributes'); - const toUp = getSymbolsToUpdate(this.component); + const toUp = getSymbolsToUpdate(model); toUp.forEach((child) => { child.setSymbolOverride(combinedKeys, { fromDataSource: true }); }); - this.component.setSymbolOverride(combinedKeys, { fromDataSource: true }); + model.setSymbolOverride(combinedKeys, { fromDataSource: true }); } onCollectionsStateMapUpdate() { this.propertyWatcher.onCollectionsStateMapUpdate(); this.attributeWatcher.onCollectionsStateMapUpdate(); + this.styleWatcher.onCollectionsStateMapUpdate(); } getDynamicPropsDefs() { @@ -96,6 +117,10 @@ export class ComponentDataResolverWatchers { return this.attributeWatcher.getAllSerializableValues(); } + getDynamicStylesDefs() { + return this.styleWatcher.getAllSerializableValues(); + } + getPropsDefsOrValues(props: ObjectAny) { return this.propertyWatcher.getSerializableValues(props); } @@ -104,8 +129,13 @@ export class ComponentDataResolverWatchers { return this.attributeWatcher.getSerializableValues(attributes); } + getStylesDefsOrValues(styles: ObjectAny) { + return this.styleWatcher.getSerializableValues(styles); + } + destroy() { this.propertyWatcher.destroy(); this.attributeWatcher.destroy(); + this.styleWatcher.destroy(); } } diff --git a/packages/core/src/dom_components/model/ComponentResolverWatcher.ts b/packages/core/src/dom_components/model/ModelResolverWatcher.ts similarity index 88% rename from packages/core/src/dom_components/model/ComponentResolverWatcher.ts rename to packages/core/src/dom_components/model/ModelResolverWatcher.ts index fd26d3270..34e62ca4d 100644 --- a/packages/core/src/dom_components/model/ComponentResolverWatcher.ts +++ b/packages/core/src/dom_components/model/ModelResolverWatcher.ts @@ -1,6 +1,7 @@ import { ObjectAny } from '../../common'; import DataResolverListener from '../../data_sources/model/DataResolverListener'; import { getDataResolverInstance, getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/utils'; +import StyleableModel from '../../domain_abstract/model/StyleableModel'; import EditorModel from '../../editor/model/Editor'; import Component from './Component'; @@ -9,26 +10,27 @@ export interface DynamicWatchersOptions { fromDataSource?: boolean; } -export interface ComponentResolverWatcherOptions { +export interface ModelResolverWatcherOptions { em: EditorModel; } -type UpdateFn = (component: Component | undefined, key: string, value: any) => void; +type NewType = StyleableModel | undefined; +type UpdateFn = (component: NewType, key: string, value: any) => void; -export class ComponentResolverWatcher { +export class ModelResolverWatcher { private em: EditorModel; private resolverListeners: Record = {}; constructor( - private component: Component | undefined, + private model: NewType, private updateFn: UpdateFn, - options: ComponentResolverWatcherOptions, + options: ModelResolverWatcherOptions, ) { this.em = options.em; } - bindComponent(component: Component) { - this.component = component; + bindModel(model: StyleableModel) { + this.model = model; } setDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) { @@ -63,14 +65,13 @@ export class ComponentResolverWatcher { this.getSerializableValues(Object.fromEntries(resolvesFromCollections.map((key) => [key, null]))), ); - Object.entries(evaluatedValues).forEach(([key, value]) => this.updateFn(this.component, key, value)); + Object.entries(evaluatedValues).forEach(([key, value]) => this.updateFn(this.model, key, value)); } private get collectionsStateMap() { - const component = this.component; - if (!component) return {}; - - return component.collectionsStateMap; + const component = this.model; + if (component instanceof Component) return component.collectionsStateMap; + return {}; } private updateListeners(values: { [key: string]: any }) { @@ -90,7 +91,7 @@ export class ComponentResolverWatcher { this.resolverListeners[key] = new DataResolverListener({ em, resolver, - onUpdate: (value) => this.updateFn.bind(this)(this.component, key, value), + onUpdate: (value) => this.updateFn(this.model, key, value), }); } } diff --git a/packages/core/src/dom_components/model/types.ts b/packages/core/src/dom_components/model/types.ts index 7d7ab3942..400200410 100644 --- a/packages/core/src/dom_components/model/types.ts +++ b/packages/core/src/dom_components/model/types.ts @@ -1,4 +1,4 @@ -import { DynamicWatchersOptions } from './ComponentResolverWatcher'; +import { DynamicWatchersOptions } from './ModelResolverWatcher'; import Frame from '../../canvas/model/Frame'; import { AddOptions, Nullable, OptionAsDocument } from '../../common'; import EditorModel from '../../editor/model/Editor'; @@ -12,7 +12,6 @@ import Component from './Component'; import Components from './Components'; import { ToolbarButtonProps } from './ToolbarButton'; import { ParseNodeOptions } from '../../parser/config/config'; -import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; export type DragMode = 'translate' | 'absolute' | ''; diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index f50769902..1a79ef518 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -5,23 +5,18 @@ import Selectors from '../../selector_manager/model/Selectors'; import { shallowDiff } from '../../utils/mixins'; import EditorModel from '../../editor/model/Editor'; import { DataVariableProps } from '../../data_sources/model/DataVariable'; -import DataResolverListener from '../../data_sources/model/DataResolverListener'; import CssRuleView from '../../css_composer/view/CssRuleView'; import ComponentView from '../../dom_components/view/ComponentView'; import Frame from '../../canvas/model/Frame'; import { DataConditionProps } from '../../data_sources/model/conditional_variables/DataCondition'; -import { - getDataResolverInstance, - getDataResolverInstanceValue, - isDataResolver, - isDataResolverProps, -} from '../../data_sources/utils'; -import { DataResolver } from '../../data_sources/types'; import { ToCssOptions } from '../../css_composer/model/CssRule'; +import { ModelDataResolverWatchers } from '../../dom_components/model/ModelDataResolverWatchers'; +import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; +import { DynamicWatchersOptions } from '../../dom_components/model/ModelResolverWatcher'; export type StyleProps = Record; -export interface UpdateStyleOptions extends SetOptions { +export interface UpdateStyleOptions extends SetOptions, DynamicWatchersOptions { partial?: boolean; addStyle?: StyleProps; inline?: boolean; @@ -36,13 +31,19 @@ export const getLastStyleValue = (value: string | string[]) => { return isArray(value) ? value[value.length - 1] : value; }; -export default class StyleableModel extends Model { +export default class StyleableModel extends Model { em?: EditorModel; views: StyleableView[] = []; - styleResolverListeners: Record = {}; + dataResolverWatchers: ModelDataResolverWatchers; + collectionsStateMap: DataCollectionStateMap = {}; constructor(attributes: T, options: { em?: EditorModel } = {}) { - super(attributes, options); + const em = options.em!; + const dataResolverWatchers = new ModelDataResolverWatchers(undefined, { em }); + super(attributes, { ...options, dataResolverWatchers }); + dataResolverWatchers.bindModel(this); + dataResolverWatchers.setStyles(this.get('style')!); + this.dataResolverWatchers = dataResolverWatchers; this.em = options.em; } @@ -69,15 +70,12 @@ export default class StyleableModel extends Model * @return {Object} */ getStyle(prop?: string | ObjectAny, opts: { skipResolve?: boolean } = {}): StyleProps { - const style = this.get('style') || {}; - const result: ObjectAny = { ...style }; - - if (this.em && !opts.skipResolve) { - const resolvedStyle = this.getResolvedStyles({ ...result }); - // @ts-ignore - return prop && isString(prop) ? resolvedStyle[prop] : resolvedStyle; + const style: ObjectAny = this.get('style') || {}; + if (!opts.skipResolve) { + return prop && isString(prop) ? { ...style }[prop] : { ...style }; } + const result: ObjectAny = { ...style, ...this.dataResolverWatchers.getDynamicStylesDefs() }; return prop && isString(prop) ? result[prop] : result; } @@ -92,7 +90,7 @@ export default class StyleableModel extends Model prop = this.parseStyle(prop); } - const propOrig = this.getStyle(opts); + const propOrig = this.getStyle({ skipResolve: true }); if (opts.partial || opts.avoidStore) { opts.avoidStore = true; @@ -102,28 +100,16 @@ export default class StyleableModel extends Model } const propNew = { ...prop }; - const newStyle = { ...propNew }; + let newStyle = { ...propNew }; keys(newStyle).forEach((key) => { // Remove empty style properties - if (newStyle[key] === '') { + if (newStyle[key] === '' || key === '__p') { delete newStyle[key]; return; } - - const styleValue = newStyle[key]; - if (isDataResolverProps(styleValue)) { - const dataResolver = getDataResolverInstance(styleValue, { - em: this.em!, - collectionsStateMap: {}, - }); - - if (dataResolver) { - newStyle[key] = dataResolver; - this.listenToDataResolver(dataResolver, key); - } - } }); + newStyle = this.dataResolverWatchers.setStyles(newStyle, opts); this.set('style', newStyle, opts as any); @@ -147,19 +133,6 @@ export default class StyleableModel extends Model return newStyle; } - listenToDataResolver(resolver: DataResolver, styleProp: string) { - const resolverListener = this.styleResolverListeners[styleProp]; - if (resolverListener) { - resolverListener.listenToResolver(); - } else { - this.styleResolverListeners[styleProp] = new DataResolverListener({ - em: this.em!, - resolver, - onUpdate: () => this.updateView(), - }); - } - } - getView(frame?: Frame) { let { views, em } = this; const frm = frame || em?.getCurrentFrameModel(); @@ -180,31 +153,6 @@ export default class StyleableModel extends Model this.views.forEach((view) => view.updateStyles()); } - getResolvedStyles(style: StyleProps): StyleProps { - const resultStyle = { ...style }; - - keys(resultStyle).forEach((key) => { - const styleValue = resultStyle[key]; - - if (typeof styleValue === 'string' || Array.isArray(styleValue)) { - return; - } - - if (isDataResolverProps(styleValue)) { - resultStyle[key] = getDataResolverInstanceValue(styleValue, { - em: this.em!, - collectionsStateMap: {}, - }); - } - - if (isDataResolver(styleValue)) { - resultStyle[key] = styleValue.getDataValue(); - } - }); - - return resultStyle; - } - /** * Add style property * @param {Object|string} prop diff --git a/packages/core/test/specs/data_sources/model/StyleDataVariable.ts b/packages/core/test/specs/data_sources/model/StyleDataVariable.ts index 5f0aa3ee7..a132b323e 100644 --- a/packages/core/test/specs/data_sources/model/StyleDataVariable.ts +++ b/packages/core/test/specs/data_sources/model/StyleDataVariable.ts @@ -2,7 +2,6 @@ import Editor from '../../../../src/editor/model/Editor'; import DataSourceManager from '../../../../src/data_sources'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; -import { DataSourceProps } from '../../../../src/data_sources/types'; import { setupTestEditor } from '../../../common'; describe('StyleDataVariable', () => { @@ -25,17 +24,19 @@ describe('StyleDataVariable', () => { }; dsm.add(styleDataSource); + const initialStyle = { + color: { + type: DataVariableType, + defaultValue: 'black', + path: 'colors-data.id1.color', + }, + }; + const cmp = cmpRoot.append({ tagName: 'h1', type: 'text', content: 'Hello World', - style: { - color: { - type: DataVariableType, - defaultValue: 'black', - path: 'colors-data.id1.color', - }, - }, + style: initialStyle, })[0]; const style = cmp.getStyle(); @@ -157,62 +158,148 @@ describe('StyleDataVariable', () => { expect(updatedStyle).toHaveProperty('color', 'blue'); }); + describe('Component style manipulations', () => { + test('adding a new dynamic style with addStyle', () => { + dsm.add({ id: 'data1', records: [{ id: 'rec1', color: 'red' }] }); + const cmp = cmpRoot.append({ + style: { + color: { type: DataVariableType, path: 'data1.rec1.color' }, + }, + })[0]; + expect(cmp.getStyle()).toEqual({ color: 'red' }); + + dsm.add({ id: 'data2', records: [{ id: 'rec2', width: '100px' }] }); + cmp.addStyle({ + width: { type: DataVariableType, path: 'data2.rec2.width' }, + }); + + expect(cmp.getStyle()).toEqual({ color: 'red', width: '100px' }); + dsm.get('data1').getRecord('rec1')?.set({ color: 'blue' }); + expect(cmp.getStyle()).toEqual({ color: 'blue', width: '100px' }); + dsm.get('data2').getRecord('rec2')?.set({ width: '200px' }); + expect(cmp.getStyle()).toEqual({ color: 'blue', width: '200px' }); + }); + + test('updating a dynamic style with a static value using setStyle', () => { + dsm.add({ id: 'data1', records: [{ id: 'rec1', color: 'red' }] }); + const cmp = cmpRoot.append({ + style: { + color: { type: DataVariableType, path: 'data1.rec1.color' }, + 'font-size': '12px', + }, + })[0]; + expect(cmp.getStyle()).toEqual({ color: 'red', 'font-size': '12px' }); + + cmp.setStyle({ color: 'green', 'font-size': '12px' }); + expect(cmp.getStyle()).toEqual({ color: 'green', 'font-size': '12px' }); + + // The component should no longer be listening to the data source + dsm.get('data1').getRecord('rec1')?.set({ color: 'blue' }); + expect(cmp.getStyle()).toEqual({ color: 'green', 'font-size': '12px' }); + }); + + test('updating a static style with a dynamic value', () => { + dsm.add({ id: 'data1', records: [{ id: 'rec1', color: 'red' }] }); + const cmp = cmpRoot.append({ style: { color: 'green' } })[0]; + expect(cmp.getStyle()).toEqual({ color: 'green' }); + + cmp.setStyle({ + color: { type: DataVariableType, path: 'data1.rec1.color' }, + }); + expect(cmp.getStyle()).toEqual({ color: 'red' }); + + dsm.get('data1').getRecord('rec1')?.set({ color: 'blue' }); + expect(cmp.getStyle()).toEqual({ color: 'blue' }); + }); + + test('overwriting a dynamic style with a new dynamic style', () => { + dsm.add({ id: 'data1', records: [{ id: 'rec1', color: 'red' }] }); + dsm.add({ id: 'data2', records: [{ id: 'rec2', color: 'purple' }] }); + const cmp = cmpRoot.append({ + style: { + color: { type: DataVariableType, path: 'data1.rec1.color' }, + }, + })[0]; + expect(cmp.getStyle()).toEqual({ color: 'red' }); + + cmp.setStyle({ + color: { type: DataVariableType, path: 'data2.rec2.color' }, + }); + expect(cmp.getStyle()).toEqual({ color: 'purple' }); + + // Should no longer listen to the old data source + dsm.get('data1').getRecord('rec1')?.set({ color: 'blue' }); + expect(cmp.getStyle()).toEqual({ color: 'purple' }); + + // Should listen to the new data source + dsm.get('data2').getRecord('rec2')?.set({ color: 'orange' }); + expect(cmp.getStyle()).toEqual({ color: 'orange' }); + }); + + test('getting unresolver style values', () => { + dsm.add({ id: 'data1', records: [{ id: 'rec1', color: 'red', width: '100px' }] }); + const color = { type: DataVariableType, path: 'data1.rec1.color' }; + const cmp = cmpRoot.append({ + style: { + color, + }, + })[0]; + expect(cmp.getStyle()).toEqual({ color: 'red' }); + const width = { type: DataVariableType, path: 'data1.rec1.width' }; + cmp.setStyle({ width }); + + expect(cmp.getStyle({ skipResolve: true })).toEqual({ color, width }); + }); + }); + describe('.addToCollection', () => { - test('should add a datavariable to css rule made via .addToCollection', () => { + test('should add a datavariable to css rule and verify via CssComposer', () => { const dsId = 'globalStyles'; - const drId = 'red-header'; - const selector = 'h1'; + const drId1 = 'red-header'; + const drId2 = 'blue-paragraph'; + const selectorH1 = 'h1'; + const selectorP = 'p'; - const addToCollectionDataSource = { + dsm.add({ id: dsId, records: [ - { - id: drId, - property: 'color', - value: 'red', - selector, - label: 'Red Header', - }, + { id: drId1, value: 'red' }, + { id: drId2, value: 'blue' }, ], - }; - dsm.add(addToCollectionDataSource); + }); - cmpRoot.append({ - tagName: 'h1', - type: 'text', - content: 'Hello World', - })[0]; + cmpRoot.append([ + { tagName: 'h1', type: 'text', content: 'Hello World' }, + { tagName: 'p', type: 'text', content: 'This is a paragraph.' }, + ]); const cssComposer = em.getEditor().CssComposer; + const initialStyle1 = { + color: { type: DataVariableType, path: `${dsId}.${drId1}.value` }, + }; + const initialStyle2 = { + color: { type: DataVariableType, path: `${dsId}.${drId2}.value` }, + }; - const [rule] = cssComposer.addCollection([ - { - selectors: [], - selectorsAdd: selector, - group: `globalStyles:${drId}`, - style: { - color: { - type: DataVariableType, - defaultValue: 'black', - path: `${dsId}.${drId}.value`, - }, - }, - }, - ]); + const [rule1] = cssComposer.addCollection([{ selectors: [selectorH1], style: initialStyle1 }]); + const [rule2] = cssComposer.addCollection([{ selectors: [selectorP], style: initialStyle2 }]); cssComposer.render(); - const view = rule.getView(); + const allRules = cssComposer.getAll(); - expect(rule.getStyle()).toHaveProperty('color', 'red'); - expect(em.getEditor().getCss()).toContain(`${selector}{color:red;}`); - expect(view?.el.innerHTML).toContain(`h1{color:red;}`); + // Verify initial resolved and unresolved styles + expect(rule1.getStyle()).toHaveProperty('color', 'red'); + expect(rule2.getStyle()).toHaveProperty('color', 'blue'); + expect(allRules.at(0).getStyle('', { skipResolve: true })).toEqual(initialStyle1); + expect(allRules.at(1).getStyle('', { skipResolve: true })).toEqual(initialStyle2); + // Update data source and verify changes const ds = dsm.get(dsId); - ds.getRecord(drId)?.set({ value: 'blue' }); + ds.getRecord(drId1)?.set({ value: 'purple' }); + ds.getRecord(drId2)?.set({ value: 'orange' }); - expect(rule.getStyle()).toHaveProperty('color', 'blue'); - expect(em.getEditor().getCss()).toContain(`${selector}{color:blue;}`); - expect(view?.el.innerHTML).toContain(`h1{color:blue;}`); + expect(rule1.getStyle()).toHaveProperty('color', 'purple'); + expect(allRules.at(1).getStyle()).toHaveProperty('color', 'orange'); }); }); }); diff --git a/packages/core/test/specs/data_sources/serialization.ts b/packages/core/test/specs/data_sources/serialization.ts index 2f67b17e8..b09b53240 100644 --- a/packages/core/test/specs/data_sources/serialization.ts +++ b/packages/core/test/specs/data_sources/serialization.ts @@ -355,7 +355,6 @@ describe('DataSource Serialization', () => { test('StyleDataVariable', () => { const componentProjectData: ProjectData = { - assets: [], pages: [ { frames: [ @@ -371,28 +370,9 @@ describe('DataSource Serialization', () => { type: 'text', }, ], - docEl: { - tagName: 'html', - }, - head: { - type: 'head', - }, - stylable: [ - 'background', - 'background-color', - 'background-image', - 'background-repeat', - 'background-attachment', - 'background-position', - 'background-size', - ], - type: 'wrapper', }, - id: 'componentid', }, ], - id: 'frameid', - type: 'main', }, ], styles: [ @@ -407,18 +387,29 @@ describe('DataSource Serialization', () => { }, }, ], - symbols: [], dataSources: [styleDataSource], }; editor.loadProjectData(componentProjectData); - const components = editor.getComponents(); - const component = components.models[0]; + const component = editor.getComponents().models[0]; const style = component.getStyle(); - - expect(style).toEqual({ - color: 'red', + expect(style).toEqual({ color: 'red' }); + + // Further validation: ensure the style updates when the data source changes + const loadedDsm = editor.DataSources; + const colorsDatasource = loadedDsm.get('colors-data'); + colorsDatasource.getRecord('id1')?.set({ color: 'blue' }); + + const updatedStyle = component.getStyle(); + expect(updatedStyle).toEqual({ color: 'blue' }); + const unresolvedStyle = component.getStyle({ skipResolve: true }); + expect(unresolvedStyle).toEqual({ + color: { + path: 'colors-data.id1.color', + type: DataVariableType, + defaultValue: 'black', + }, }); }); });