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); isObject(props) && model.set(props, opts);
if (updateStyle) { 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); 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 = {}) { constructor(props: CssRuleProperties, opt: any = {}) {
super(props); super(props, { em: opt.em });
this.config = props || {}; this.config = props || {};
this.opt = opt; this.opt = opt;
this.em = opt.em; this.em = opt.em;
@ -326,7 +326,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
if (isEmpty(obj.style)) delete obj.style; 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, DataCollectionStateMap,
} from './types'; } from './types';
import { detachSymbolInstance, getSymbolInstances } from '../../../dom_components/model/SymbolUtils'; 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 { ModelDestroyOptions } from 'backbone';
import Components from '../../../dom_components/model/Components'; import Components from '../../../dom_components/model/Components';

35
packages/core/src/dom_components/model/Component.ts

@ -52,8 +52,8 @@ import {
updateSymbolProps, updateSymbolProps,
getSymbolsToUpdate, getSymbolsToUpdate,
} from './SymbolUtils'; } from './SymbolUtils';
import { ComponentDataResolverWatchers } from './ComponentDataResolverWatchers'; import { ModelDataResolverWatchers } from './ModelDataResolverWatchers';
import { DynamicWatchersOptions } from './ComponentResolverWatcher'; import { DynamicWatchersOptions } from './ModelResolverWatcher';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import { checkAndGetSyncableCollectionItemId } from '../../data_sources/utils'; import { checkAndGetSyncableCollectionItemId } from '../../data_sources/utils';
@ -264,18 +264,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @private * @private
* @ts-ignore */ * @ts-ignore */
collection!: Components; collection!: Components;
dataResolverWatchers: ComponentDataResolverWatchers;
collectionsStateMap: DataCollectionStateMap = {};
constructor(props: ComponentProperties = {}, opt: ComponentOptions) { constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
const em = opt.em; const em = opt.em;
const dataResolverWatchers = new ComponentDataResolverWatchers(undefined, { em }); super(props, opt);
super(props, {
...opt,
dataResolverWatchers,
} as any);
dataResolverWatchers.bindComponent(this);
this.dataResolverWatchers = dataResolverWatchers;
bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps', 'syncOnComponentChange'); bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps', 'syncOnComponentChange');
@ -344,6 +336,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
isSymbol(this) && initSymbol(this); isSymbol(this) && initSymbol(this);
em?.trigger(ComponentsEvents.create, this, opt); em?.trigger(ComponentsEvents.create, this, opt);
} }
if (avoidInline(em)) {
this.dataResolverWatchers.disableStyles();
}
} }
set<A extends string>( set<A extends string>(
@ -353,7 +349,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
): this { ): this {
let attributes: Partial<ComponentProperties>; let attributes: Partial<ComponentProperties>;
let options: ComponentSetOptions & { let options: ComponentSetOptions & {
dataResolverWatchers?: ComponentDataResolverWatchers; dataResolverWatchers?: ModelDataResolverWatchers;
} = { skipWatcherUpdates: false, fromDataSource: false }; } = { skipWatcherUpdates: false, fromDataSource: false };
if (typeof keyOrAttributes === 'object') { if (typeof keyOrAttributes === 'object') {
attributes = keyOrAttributes; attributes = keyOrAttributes;
@ -368,7 +364,6 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.dataResolverWatchers = this.dataResolverWatchers || options.dataResolverWatchers; this.dataResolverWatchers = this.dataResolverWatchers || options.dataResolverWatchers;
const evaluatedProps = this.dataResolverWatchers.addProps(attributes, options); const evaluatedProps = this.dataResolverWatchers.addProps(attributes, options);
return super.set(evaluatedProps, options); return super.set(evaluatedProps, options);
} }
@ -812,8 +807,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/ */
getStyle(options: any = {}, optsAdd: any = {}) { getStyle(options: any = {}, optsAdd: any = {}) {
const { em } = this; const { em } = this;
const prop = isString(options) ? options : ''; const isOptionsString = isString(options);
const opts = prop ? optsAdd : options; const prop = isOptionsString ? options : '';
const opts = isOptionsString || options === '' ? optsAdd : options;
const skipResolve = !!opts?.skipResolve;
if (avoidInline(em) && !opts.inline) { if (avoidInline(em) && !opts.inline) {
const state = em.get('state'); const state = em.get('state');
@ -822,15 +819,15 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.rule = rule; this.rule = rule;
if (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 // because else on load inline style is set a #id or .class style
return {}; 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) }; prop = { ...prop, ...(style as any) };
const state = em.get('state'); const state = em.get('state');
const cc = em.Css; const cc = em.Css;
const propOrig = this.getStyle(opts); const propOrig = this.getStyle({ ...opts, skipResolve: true });
const newStyle = { ...propOrig, ...prop }; const newStyle = { ...propOrig, ...prop };
this.rule = cc.setIdRule(this.getId(), newStyle, { state, ...opts }); this.rule = cc.setIdRule(this.getId(), newStyle, { state, ...opts });
const diff = shallowDiff(propOrig, prop); 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 { ObjectAny } from '../../common';
import Component from './Component'; import StyleableModel from '../../domain_abstract/model/StyleableModel';
import { import {
ComponentResolverWatcher, ModelResolverWatcher as ModelResolverWatcher,
ComponentResolverWatcherOptions, ModelResolverWatcherOptions,
DynamicWatchersOptions, DynamicWatchersOptions,
} from './ComponentResolverWatcher'; } from './ModelResolverWatcher';
import { getSymbolsToUpdate } from './SymbolUtils'; import { getSymbolsToUpdate } from './SymbolUtils';
export const updateFromWatcher = { fromDataSource: true, avoidStore: true }; export const updateFromWatcher = { fromDataSource: true, avoidStore: true };
export class ComponentDataResolverWatchers { export class ModelDataResolverWatchers {
private propertyWatcher: ComponentResolverWatcher; private propertyWatcher: ModelResolverWatcher;
private attributeWatcher: ComponentResolverWatcher; private attributeWatcher: ModelResolverWatcher;
private styleWatcher: ModelResolverWatcher;
constructor( constructor(
private component: Component | undefined, private model: StyleableModel | undefined,
options: ComponentResolverWatcherOptions, options: ModelResolverWatcherOptions,
) { ) {
this.propertyWatcher = new ComponentResolverWatcher(component, this.onPropertyUpdate, options); this.propertyWatcher = new ModelResolverWatcher(model, this.onPropertyUpdate, options);
this.attributeWatcher = new ComponentResolverWatcher(component, this.onAttributeUpdate, 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); component?.set(key, value, updateFromWatcher);
} }
private onAttributeUpdate(component: Component | undefined, key: string, value: any) { private onAttributeUpdate(component: StyleableModel | undefined, key: string, value: any) {
component?.addAttributes({ [key]: value }, updateFromWatcher); (component as any)?.addAttributes({ [key]: value }, updateFromWatcher);
} }
bindComponent(component: Component) { private onStyleUpdate(component: StyleableModel | undefined, key: string, value: any) {
this.component = component; component?.addStyle({ [key]: value }, { ...updateFromWatcher, noEvent: true, partial: true, avoidStore: true });
this.propertyWatcher.bindComponent(component); }
this.attributeWatcher.bindComponent(component);
bindModel(model: StyleableModel) {
this.model = model;
this.propertyWatcher.bindModel(model);
this.attributeWatcher.bindModel(model);
this.styleWatcher.bindModel(model);
this.updateSymbolOverride(); this.updateSymbolOverride();
} }
@ -60,14 +67,27 @@ export class ComponentDataResolverWatchers {
return evaluatedProps; 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[]) { removeAttributes(attributes: string[]) {
this.attributeWatcher.removeListeners(attributes); this.attributeWatcher.removeListeners(attributes);
this.updateSymbolOverride(); this.updateSymbolOverride();
} }
private updateSymbolOverride() { private updateSymbolOverride() {
const isCollectionItem = !!Object.keys(this.component?.collectionsStateMap ?? {}).length; const model = this.model as any;
if (!this.component || !isCollectionItem) return; const isCollectionItem = !!Object.keys(model?.collectionsStateMap ?? {}).length;
if (!this.model || !isCollectionItem) return;
const keys = this.propertyWatcher.getValuesResolvingFromCollections(); const keys = this.propertyWatcher.getValuesResolvingFromCollections();
const attributesKeys = this.attributeWatcher.getValuesResolvingFromCollections(); const attributesKeys = this.attributeWatcher.getValuesResolvingFromCollections();
@ -76,16 +96,17 @@ export class ComponentDataResolverWatchers {
const haveOverridenAttributes = Object.keys(attributesKeys).length; const haveOverridenAttributes = Object.keys(attributesKeys).length;
if (haveOverridenAttributes) combinedKeys.push('attributes'); if (haveOverridenAttributes) combinedKeys.push('attributes');
const toUp = getSymbolsToUpdate(this.component); const toUp = getSymbolsToUpdate(model);
toUp.forEach((child) => { toUp.forEach((child) => {
child.setSymbolOverride(combinedKeys, { fromDataSource: true }); child.setSymbolOverride(combinedKeys, { fromDataSource: true });
}); });
this.component.setSymbolOverride(combinedKeys, { fromDataSource: true }); model.setSymbolOverride(combinedKeys, { fromDataSource: true });
} }
onCollectionsStateMapUpdate() { onCollectionsStateMapUpdate() {
this.propertyWatcher.onCollectionsStateMapUpdate(); this.propertyWatcher.onCollectionsStateMapUpdate();
this.attributeWatcher.onCollectionsStateMapUpdate(); this.attributeWatcher.onCollectionsStateMapUpdate();
this.styleWatcher.onCollectionsStateMapUpdate();
} }
getDynamicPropsDefs() { getDynamicPropsDefs() {
@ -96,6 +117,10 @@ export class ComponentDataResolverWatchers {
return this.attributeWatcher.getAllSerializableValues(); return this.attributeWatcher.getAllSerializableValues();
} }
getDynamicStylesDefs() {
return this.styleWatcher.getAllSerializableValues();
}
getPropsDefsOrValues(props: ObjectAny) { getPropsDefsOrValues(props: ObjectAny) {
return this.propertyWatcher.getSerializableValues(props); return this.propertyWatcher.getSerializableValues(props);
} }
@ -104,8 +129,13 @@ export class ComponentDataResolverWatchers {
return this.attributeWatcher.getSerializableValues(attributes); return this.attributeWatcher.getSerializableValues(attributes);
} }
getStylesDefsOrValues(styles: ObjectAny) {
return this.styleWatcher.getSerializableValues(styles);
}
destroy() { destroy() {
this.propertyWatcher.destroy(); this.propertyWatcher.destroy();
this.attributeWatcher.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 { ObjectAny } from '../../common';
import DataResolverListener from '../../data_sources/model/DataResolverListener'; import DataResolverListener from '../../data_sources/model/DataResolverListener';
import { getDataResolverInstance, getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/utils'; import { getDataResolverInstance, getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/utils';
import StyleableModel from '../../domain_abstract/model/StyleableModel';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import Component from './Component'; import Component from './Component';
@ -9,26 +10,27 @@ export interface DynamicWatchersOptions {
fromDataSource?: boolean; fromDataSource?: boolean;
} }
export interface ComponentResolverWatcherOptions { export interface ModelResolverWatcherOptions {
em: EditorModel; 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 em: EditorModel;
private resolverListeners: Record<string, DataResolverListener> = {}; private resolverListeners: Record<string, DataResolverListener> = {};
constructor( constructor(
private component: Component | undefined, private model: NewType,
private updateFn: UpdateFn, private updateFn: UpdateFn,
options: ComponentResolverWatcherOptions, options: ModelResolverWatcherOptions,
) { ) {
this.em = options.em; this.em = options.em;
} }
bindComponent(component: Component) { bindModel(model: StyleableModel) {
this.component = component; this.model = model;
} }
setDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) { setDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) {
@ -63,14 +65,13 @@ export class ComponentResolverWatcher {
this.getSerializableValues(Object.fromEntries(resolvesFromCollections.map((key) => [key, null]))), 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() { private get collectionsStateMap() {
const component = this.component; const component = this.model;
if (!component) return {}; if (component instanceof Component) return component.collectionsStateMap;
return {};
return component.collectionsStateMap;
} }
private updateListeners(values: { [key: string]: any }) { private updateListeners(values: { [key: string]: any }) {
@ -90,7 +91,7 @@ export class ComponentResolverWatcher {
this.resolverListeners[key] = new DataResolverListener({ this.resolverListeners[key] = new DataResolverListener({
em, em,
resolver, 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 Frame from '../../canvas/model/Frame';
import { AddOptions, Nullable, OptionAsDocument } from '../../common'; import { AddOptions, Nullable, OptionAsDocument } from '../../common';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
@ -12,7 +12,6 @@ import Component from './Component';
import Components from './Components'; import Components from './Components';
import { ToolbarButtonProps } from './ToolbarButton'; import { ToolbarButtonProps } from './ToolbarButton';
import { ParseNodeOptions } from '../../parser/config/config'; import { ParseNodeOptions } from '../../parser/config/config';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
export type DragMode = 'translate' | 'absolute' | ''; 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 { shallowDiff } from '../../utils/mixins';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import { DataVariableProps } from '../../data_sources/model/DataVariable'; import { DataVariableProps } from '../../data_sources/model/DataVariable';
import DataResolverListener from '../../data_sources/model/DataResolverListener';
import CssRuleView from '../../css_composer/view/CssRuleView'; import CssRuleView from '../../css_composer/view/CssRuleView';
import ComponentView from '../../dom_components/view/ComponentView'; import ComponentView from '../../dom_components/view/ComponentView';
import Frame from '../../canvas/model/Frame'; import Frame from '../../canvas/model/Frame';
import { DataConditionProps } from '../../data_sources/model/conditional_variables/DataCondition'; 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 { 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 type StyleProps = Record<string, string | string[] | DataVariableProps | DataConditionProps>;
export interface UpdateStyleOptions extends SetOptions { export interface UpdateStyleOptions extends SetOptions, DynamicWatchersOptions {
partial?: boolean; partial?: boolean;
addStyle?: StyleProps; addStyle?: StyleProps;
inline?: boolean; inline?: boolean;
@ -36,13 +31,19 @@ export const getLastStyleValue = (value: string | string[]) => {
return isArray(value) ? value[value.length - 1] : value; 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; em?: EditorModel;
views: StyleableView[] = []; views: StyleableView[] = [];
styleResolverListeners: Record<string, DataResolverListener> = {}; dataResolverWatchers: ModelDataResolverWatchers;
collectionsStateMap: DataCollectionStateMap = {};
constructor(attributes: T, options: { em?: EditorModel } = {}) { 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; this.em = options.em;
} }
@ -69,15 +70,12 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
* @return {Object} * @return {Object}
*/ */
getStyle(prop?: string | ObjectAny, opts: { skipResolve?: boolean } = {}): StyleProps { getStyle(prop?: string | ObjectAny, opts: { skipResolve?: boolean } = {}): StyleProps {
const style = this.get('style') || {}; const style: ObjectAny = this.get('style') || {};
const result: ObjectAny = { ...style }; if (!opts.skipResolve) {
return prop && isString(prop) ? { ...style }[prop] : { ...style };
if (this.em && !opts.skipResolve) {
const resolvedStyle = this.getResolvedStyles({ ...result });
// @ts-ignore
return prop && isString(prop) ? resolvedStyle[prop] : resolvedStyle;
} }
const result: ObjectAny = { ...style, ...this.dataResolverWatchers.getDynamicStylesDefs() };
return prop && isString(prop) ? result[prop] : result; 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); prop = this.parseStyle(prop);
} }
const propOrig = this.getStyle(opts); const propOrig = this.getStyle({ skipResolve: true });
if (opts.partial || opts.avoidStore) { if (opts.partial || opts.avoidStore) {
opts.avoidStore = true; opts.avoidStore = true;
@ -102,28 +100,16 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
} }
const propNew = { ...prop }; const propNew = { ...prop };
const newStyle = { ...propNew }; let newStyle = { ...propNew };
keys(newStyle).forEach((key) => { keys(newStyle).forEach((key) => {
// Remove empty style properties // Remove empty style properties
if (newStyle[key] === '') { if (newStyle[key] === '' || key === '__p') {
delete newStyle[key]; delete newStyle[key];
return; 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); this.set('style', newStyle, opts as any);
@ -147,19 +133,6 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
return newStyle; 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) { getView(frame?: Frame) {
let { views, em } = this; let { views, em } = this;
const frm = frame || em?.getCurrentFrameModel(); 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()); 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 * Add style property
* @param {Object|string} prop * @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 DataSourceManager from '../../../../src/data_sources';
import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper';
import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable';
import { DataSourceProps } from '../../../../src/data_sources/types';
import { setupTestEditor } from '../../../common'; import { setupTestEditor } from '../../../common';
describe('StyleDataVariable', () => { describe('StyleDataVariable', () => {
@ -25,17 +24,19 @@ describe('StyleDataVariable', () => {
}; };
dsm.add(styleDataSource); dsm.add(styleDataSource);
const initialStyle = {
color: {
type: DataVariableType,
defaultValue: 'black',
path: 'colors-data.id1.color',
},
};
const cmp = cmpRoot.append({ const cmp = cmpRoot.append({
tagName: 'h1', tagName: 'h1',
type: 'text', type: 'text',
content: 'Hello World', content: 'Hello World',
style: { style: initialStyle,
color: {
type: DataVariableType,
defaultValue: 'black',
path: 'colors-data.id1.color',
},
},
})[0]; })[0];
const style = cmp.getStyle(); const style = cmp.getStyle();
@ -157,62 +158,148 @@ describe('StyleDataVariable', () => {
expect(updatedStyle).toHaveProperty('color', 'blue'); 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', () => { 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 dsId = 'globalStyles';
const drId = 'red-header'; const drId1 = 'red-header';
const selector = 'h1'; const drId2 = 'blue-paragraph';
const selectorH1 = 'h1';
const selectorP = 'p';
const addToCollectionDataSource = { dsm.add({
id: dsId, id: dsId,
records: [ records: [
{ { id: drId1, value: 'red' },
id: drId, { id: drId2, value: 'blue' },
property: 'color',
value: 'red',
selector,
label: 'Red Header',
},
], ],
}; });
dsm.add(addToCollectionDataSource);
cmpRoot.append({ cmpRoot.append([
tagName: 'h1', { tagName: 'h1', type: 'text', content: 'Hello World' },
type: 'text', { tagName: 'p', type: 'text', content: 'This is a paragraph.' },
content: 'Hello World', ]);
})[0];
const cssComposer = em.getEditor().CssComposer; 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([ const [rule1] = cssComposer.addCollection([{ selectors: [selectorH1], style: initialStyle1 }]);
{ const [rule2] = cssComposer.addCollection([{ selectors: [selectorP], style: initialStyle2 }]);
selectors: [],
selectorsAdd: selector,
group: `globalStyles:${drId}`,
style: {
color: {
type: DataVariableType,
defaultValue: 'black',
path: `${dsId}.${drId}.value`,
},
},
},
]);
cssComposer.render(); cssComposer.render();
const view = rule.getView(); const allRules = cssComposer.getAll();
expect(rule.getStyle()).toHaveProperty('color', 'red'); // Verify initial resolved and unresolved styles
expect(em.getEditor().getCss()).toContain(`${selector}{color:red;}`); expect(rule1.getStyle()).toHaveProperty('color', 'red');
expect(view?.el.innerHTML).toContain(`h1{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); 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(rule1.getStyle()).toHaveProperty('color', 'purple');
expect(em.getEditor().getCss()).toContain(`${selector}{color:blue;}`); expect(allRules.at(1).getStyle()).toHaveProperty('color', 'orange');
expect(view?.el.innerHTML).toContain(`h1{color:blue;}`);
}); });
}); });
}); });

43
packages/core/test/specs/data_sources/serialization.ts

@ -355,7 +355,6 @@ describe('DataSource Serialization', () => {
test('StyleDataVariable', () => { test('StyleDataVariable', () => {
const componentProjectData: ProjectData = { const componentProjectData: ProjectData = {
assets: [],
pages: [ pages: [
{ {
frames: [ frames: [
@ -371,28 +370,9 @@ describe('DataSource Serialization', () => {
type: 'text', 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: [ styles: [
@ -407,18 +387,29 @@ describe('DataSource Serialization', () => {
}, },
}, },
], ],
symbols: [],
dataSources: [styleDataSource], dataSources: [styleDataSource],
}; };
editor.loadProjectData(componentProjectData); editor.loadProjectData(componentProjectData);
const components = editor.getComponents(); const component = editor.getComponents().models[0];
const component = components.models[0];
const style = component.getStyle(); 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