Browse Source

Update component styles (#6531)

* Add style watchers to modelResolverWatcher class

* Remove watcher init from Component

* Add watcher init to styleableModel and refactor styleableModel

* Fix global styles listening to datasources

* cleanup

* fix initial styles not working bug

* Add skip resolve option to component.getStyle

* Add more global styles unit tests

* Improve serialization test
pull/6544/head
mohamed yahia 1 year ago
committed by GitHub
parent
commit
afad35cafa
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      packages/core/src/css_composer/index.ts
  2. 4
      packages/core/src/css_composer/model/CssRule.ts
  3. 2
      packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts
  4. 35
      packages/core/src/dom_components/model/Component.ts
  5. 74
      packages/core/src/dom_components/model/ModelDataResolverWatchers.ts
  6. 27
      packages/core/src/dom_components/model/ModelResolverWatcher.ts
  7. 3
      packages/core/src/dom_components/model/types.ts
  8. 94
      packages/core/src/domain_abstract/model/StyleableModel.ts
  9. 183
      packages/core/test/specs/data_sources/model/StyleDataVariable.ts
  10. 43
      packages/core/test/specs/data_sources/serialization.ts

2
packages/core/src/css_composer/index.ts

@ -262,7 +262,7 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
isObject(props) && model.set(props, opts);
if (updateStyle) {
const styleUpdate = opts.extend ? { ...model.get('style'), ...style } : style;
const styleUpdate = opts.extend ? { ...model.getStyle('', { skipResolve: true }), ...style } : style;
model.setStyle(styleUpdate, opts);
}

4
packages/core/src/css_composer/model/CssRule.ts

@ -120,7 +120,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
}
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<CssRuleProperties> {
if (isEmpty(obj.style)) delete obj.style;
}
return obj;
return { ...obj, style: this.dataResolverWatchers.getStylesDefsOrValues(obj.style) };
}
/**

2
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';

35
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<ComponentProperties> {
* @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<ComponentProperties> {
isSymbol(this) && initSymbol(this);
em?.trigger(ComponentsEvents.create, this, opt);
}
if (avoidInline(em)) {
this.dataResolverWatchers.disableStyles();
}
}
set<A extends string>(
@ -353,7 +349,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
): this {
let attributes: Partial<ComponentProperties>;
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<ComponentProperties> {
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<ComponentProperties> {
*/
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<ComponentProperties> {
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<ComponentProperties> {
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);

74
packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts → 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();
}
}

27
packages/core/src/dom_components/model/ComponentResolverWatcher.ts → 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<string, DataResolverListener> = {};
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),
});
}
}

3
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' | '';

94
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<string, string | string[] | DataVariableProps | DataConditionProps>;
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<T extends ObjectHash = any> extends Model<T> {
export default class StyleableModel<T extends ObjectHash = any> extends Model<T, UpdateStyleOptions> {
em?: EditorModel;
views: StyleableView[] = [];
styleResolverListeners: Record<string, DataResolverListener> = {};
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<T extends ObjectHash = any> extends Model<T>
* @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<T extends ObjectHash = any> extends Model<T>
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<T extends ObjectHash = any> extends Model<T>
}
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<T extends ObjectHash = any> extends Model<T>
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<T extends ObjectHash = any> extends Model<T>
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

183
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');
});
});
});

43
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',
},
});
});
});

Loading…
Cancel
Save