From ef26212f39fbb0a44c480bb185b5d1481f67eb40 Mon Sep 17 00:00:00 2001 From: mohamed yahia Date: Mon, 4 Nov 2024 16:07:41 +0200 Subject: [PATCH] Add conditional variables (#6282) --- .../model/ComponentDataVariable.ts | 7 +- .../src/data_sources/model/DataVariable.ts | 7 +- .../model/DataVariableListenerManager.ts | 19 +- .../data_sources/model/TraitDataVariable.ts | 7 +- .../model/conditional_variables/Condition.ts | 12 +- .../ConditionalComponent.ts | 46 +++ .../conditional_variables/DataCondition.ts | 73 +++-- packages/core/src/data_sources/model/utils.ts | 13 +- packages/core/src/data_sources/types.ts | 5 + .../data_sources/view/ComponentDynamicView.ts | 4 + packages/core/src/dom_components/index.ts | 8 + .../src/dom_components/model/Component.ts | 31 +- .../domain_abstract/model/StyleableModel.ts | 68 +++-- .../core/src/trait_manager/model/Trait.ts | 13 +- .../__snapshots__/serialization.ts.snap | 2 +- .../ComponentConditionalVariable.ts | 242 +++++++++++++++ .../ConditionalStyles.ts | 135 +++++++++ .../ConditionalTraits.ts | 283 ++++++++++++++++++ .../__snapshots__/ConditionalTraits.ts.snap | 59 ++++ .../test/specs/data_sources/serialization.ts | 7 +- 20 files changed, 959 insertions(+), 82 deletions(-) create mode 100644 packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts create mode 100644 packages/core/src/data_sources/view/ComponentDynamicView.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts create mode 100644 packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap diff --git a/packages/core/src/data_sources/model/ComponentDataVariable.ts b/packages/core/src/data_sources/model/ComponentDataVariable.ts index e75d57a60b..ac118f4c19 100644 --- a/packages/core/src/data_sources/model/ComponentDataVariable.ts +++ b/packages/core/src/data_sources/model/ComponentDataVariable.ts @@ -1,5 +1,4 @@ import Component from '../../dom_components/model/Component'; -import { ToHTMLOptions } from '../../dom_components/model/types'; import { toLowerCase } from '../../utils/mixins'; import { DataVariableType } from './DataVariable'; @@ -19,10 +18,8 @@ export default class ComponentDataVariable extends Component { return this.em.DataSources.getValue(path, defaultValue); } - getInnerHTML(opts: ToHTMLOptions) { - const val = this.getDataValue(); - - return val; + getInnerHTML() { + return this.getDataValue(); } static isComponent(el: HTMLElement) { diff --git a/packages/core/src/data_sources/model/DataVariable.ts b/packages/core/src/data_sources/model/DataVariable.ts index c84ef0b3c6..915642ebf3 100644 --- a/packages/core/src/data_sources/model/DataVariable.ts +++ b/packages/core/src/data_sources/model/DataVariable.ts @@ -3,6 +3,11 @@ import EditorModel from '../../editor/model/Editor'; import { stringToPath } from '../../utils/mixins'; export const DataVariableType = 'data-variable'; +export type DataVariableDefinition = { + type: typeof DataVariableType; + path: string; + defaultValue?: string; +}; export default class DataVariable extends Model { em?: EditorModel; @@ -15,7 +20,7 @@ export default class DataVariable extends Model { }; } - constructor(attrs: any, options: any) { + constructor(attrs: DataVariableDefinition, options: any) { super(attrs, options); this.em = options.em; this.listenToDataSource(); diff --git a/packages/core/src/data_sources/model/DataVariableListenerManager.ts b/packages/core/src/data_sources/model/DataVariableListenerManager.ts index 45a5b1e8fa..46d1528b91 100644 --- a/packages/core/src/data_sources/model/DataVariableListenerManager.ts +++ b/packages/core/src/data_sources/model/DataVariableListenerManager.ts @@ -4,12 +4,14 @@ import { Model } from '../../common'; import EditorModel from '../../editor/model/Editor'; import DataVariable, { DataVariableType } from './DataVariable'; import ComponentView from '../../dom_components/view/ComponentView'; +import { DynamicValue } from '../types'; +import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition'; import ComponentDataVariable from './ComponentDataVariable'; export interface DynamicVariableListenerManagerOptions { model: Model | ComponentView; em: EditorModel; - dataVariable: DataVariable | ComponentDataVariable; + dataVariable: DynamicValue; updateValueFromDataVariable: (value: any) => void; } @@ -17,7 +19,7 @@ export default class DynamicVariableListenerManager { private dataListeners: DataVariableListener[] = []; private em: EditorModel; private model: Model | ComponentView; - private dynamicVariable: DataVariable | ComponentDataVariable; + private dynamicVariable: DynamicValue; private updateValueFromDynamicVariable: (value: any) => void; constructor(options: DynamicVariableListenerManagerOptions) { @@ -42,7 +44,10 @@ export default class DynamicVariableListenerManager { let dataListeners: DataVariableListener[] = []; switch (type) { case DataVariableType: - dataListeners = this.listenToDataVariable(dynamicVariable, em); + dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em); + break; + case ConditionalVariableType: + dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em); break; } dataListeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange)); @@ -50,6 +55,14 @@ export default class DynamicVariableListenerManager { this.dataListeners = dataListeners; } + private listenToConditionalVariable(dataVariable: DataCondition, em: EditorModel) { + const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => { + return this.listenToDataVariable(new DataVariable(dataVariable, { em: this.em }), em); + }); + + return dataListeners; + } + private listenToDataVariable(dataVariable: DataVariable | ComponentDataVariable, em: EditorModel) { const dataListeners: DataVariableListener[] = []; const { path } = dataVariable.attributes; diff --git a/packages/core/src/data_sources/model/TraitDataVariable.ts b/packages/core/src/data_sources/model/TraitDataVariable.ts index b8aedc1f5a..f213e4b733 100644 --- a/packages/core/src/data_sources/model/TraitDataVariable.ts +++ b/packages/core/src/data_sources/model/TraitDataVariable.ts @@ -1,10 +1,13 @@ -import DataVariable from './DataVariable'; +import DataVariable, { DataVariableDefinition } from './DataVariable'; import Trait from '../../trait_manager/model/Trait'; +import { TraitProperties } from '../../trait_manager/types'; + +export type TraitDataVariableDefinition = TraitProperties & DataVariableDefinition; export default class TraitDataVariable extends DataVariable { trait?: Trait; - constructor(attrs: any, options: any) { + constructor(attrs: TraitDataVariableDefinition, options: any) { super(attrs, options); this.trait = options.trait; } diff --git a/packages/core/src/data_sources/model/conditional_variables/Condition.ts b/packages/core/src/data_sources/model/conditional_variables/Condition.ts index 4586f804b2..6af9152925 100644 --- a/packages/core/src/data_sources/model/conditional_variables/Condition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/Condition.ts @@ -1,5 +1,5 @@ +import { DataVariableDefinition, DataVariableType } from './../DataVariable'; import EditorModel from '../../../editor/model/Editor'; -import DataVariable from '../DataVariable'; import { evaluateVariable, isDataVariable } from '../utils'; import { Expression, LogicGroup } from './DataCondition'; import { LogicalGroupStatement } from './LogicalGroupStatement'; @@ -8,12 +8,14 @@ import { GenericOperation, GenericOperator } from './operators/GenericOperator'; import { LogicalOperator } from './operators/LogicalOperator'; import { NumberOperator, NumberOperation } from './operators/NumberOperator'; import { StringOperator, StringOperation } from './operators/StringOperations'; +import { Model } from '../../../common'; -export class Condition { +export class Condition extends Model { private condition: Expression | LogicGroup | boolean; private em: EditorModel; constructor(condition: Expression | LogicGroup | boolean, opts: { em: EditorModel }) { + super(condition); this.condition = condition; this.em = opts.em; } @@ -65,8 +67,8 @@ export class Condition { /** * Extracts all data variables from the condition, including nested ones. */ - getDataVariables(): DataVariable[] { - const variables: DataVariable[] = []; + getDataVariables() { + const variables: DataVariableDefinition[] = []; this.extractVariables(this.condition, variables); return variables; } @@ -74,7 +76,7 @@ export class Condition { /** * Recursively extracts variables from expressions or logic groups. */ - private extractVariables(condition: boolean | LogicGroup | Expression, variables: DataVariable[]): void { + private extractVariables(condition: boolean | LogicGroup | Expression, variables: DataVariableDefinition[]): void { if (this.isExpression(condition)) { if (isDataVariable(condition.left)) variables.push(condition.left); if (isDataVariable(condition.right)) variables.push(condition.right); diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts new file mode 100644 index 0000000000..ccf4142b93 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts @@ -0,0 +1,46 @@ +import Component from '../../../dom_components/model/Component'; +import Components from '../../../dom_components/model/Components'; +import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types'; +import { toLowerCase } from '../../../utils/mixins'; +import { DataCondition, ConditionalVariableType, Expression, LogicGroup } from './DataCondition'; + +type ConditionalComponentDefinition = { + condition: Expression | LogicGroup | boolean; + ifTrue: any; + ifFalse: any; +}; + +export default class ComponentConditionalVariable extends Component { + dataCondition: DataCondition; + componentDefinition: ConditionalComponentDefinition; + + constructor(componentDefinition: ConditionalComponentDefinition, opt: ComponentOptions) { + const { condition, ifTrue, ifFalse } = componentDefinition; + const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em }); + const initialComponentsProps = dataConditionInstance.getDataValue(); + const conditionalCmptDef = { + type: ConditionalVariableType, + components: initialComponentsProps, + }; + super(conditionalCmptDef, opt); + + this.componentDefinition = componentDefinition; + this.dataCondition = dataConditionInstance; + this.dataCondition.onValueChange = this.handleConditionChange.bind(this); + } + + private handleConditionChange() { + this.dataCondition.reevaluate(); + const updatedComponents = this.dataCondition.getDataValue(); + this.components().reset(); + this.components().add(updatedComponents); + } + + static isComponent(el: HTMLElement) { + return toLowerCase(el.tagName) === ConditionalVariableType; + } + + toJSON(): ComponentDefinition { + return this.dataCondition.toJSON(); + } +} diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 745caae342..5630198f17 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -6,10 +6,10 @@ import { LogicalOperation } from './operators/LogicalOperator'; import DynamicVariableListenerManager from '../DataVariableListenerManager'; import EditorModel from '../../../editor/model/Editor'; import { Condition } from './Condition'; -import DataVariable from '../DataVariable'; +import DataVariable, { DataVariableDefinition } from '../DataVariable'; import { evaluateVariable, isDataVariable } from '../utils'; -export const DataConditionType = 'conditional-variable'; +export const ConditionalVariableType = 'conditional-variable'; export type Expression = { left: any; operator: GenericOperation | StringOperation | NumberOperation; @@ -21,30 +21,43 @@ export type LogicGroup = { statements: (Expression | LogicGroup | boolean)[]; }; +export type ConditionalVariableDefinition = { + type: typeof ConditionalVariableType; + condition: Expression | LogicGroup | boolean; + ifTrue: any; + ifFalse: any; +}; + export class DataCondition extends Model { - private conditionResult: boolean; + lastEvaluationResult: boolean; private condition: Condition; private em: EditorModel; private variableListeners: DynamicVariableListenerManager[] = []; + private _onValueChange?: () => void; defaults() { return { - type: DataConditionType, + type: ConditionalVariableType, condition: false, }; } constructor( condition: Expression | LogicGroup | boolean, - private ifTrue: any, - private ifFalse: any, - opts: { em: EditorModel }, + public ifTrue: any, + public ifFalse: any, + opts: { em: EditorModel; onValueChange?: () => void }, ) { + if (typeof condition === 'undefined') { + throw new MissingConditionError(); + } + super(); this.condition = new Condition(condition, { em: opts.em }); this.em = opts.em; - this.conditionResult = this.evaluate(); + this.lastEvaluationResult = this.evaluate(); this.listenToDataVariables(); + this._onValueChange = opts.onValueChange; } evaluate() { @@ -52,19 +65,16 @@ export class DataCondition extends Model { } getDataValue(): any { - return this.conditionResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em); + return this.lastEvaluationResult ? evaluateVariable(this.ifTrue, this.em) : evaluateVariable(this.ifFalse, this.em); } reevaluate(): void { - this.conditionResult = this.evaluate(); + this.lastEvaluationResult = this.evaluate(); } - toJSON() { - return { - condition: this.condition, - ifTrue: this.ifTrue, - ifFalse: this.ifFalse, - }; + set onValueChange(newFunction: () => void) { + this._onValueChange = newFunction; + this.listenToDataVariables(); } private listenToDataVariables() { @@ -73,9 +83,7 @@ export class DataCondition extends Model { // Clear previous listeners to avoid memory leaks this.cleanupListeners(); - const dataVariables = this.condition.getDataVariables(); - if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); - if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); + const dataVariables = this.getDependentDataVariables(); dataVariables.forEach((variable) => { const variableInstance = new DataVariable(variable, { em: this.em }); @@ -83,15 +91,40 @@ export class DataCondition extends Model { model: this as any, em: this.em!, dataVariable: variableInstance, - updateValueFromDataVariable: this.reevaluate.bind(this), + updateValueFromDataVariable: (() => { + this.reevaluate(); + this._onValueChange?.(); + }).bind(this), }); this.variableListeners.push(listener); }); } + getDependentDataVariables() { + const dataVariables: DataVariableDefinition[] = this.condition.getDataVariables(); + if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); + if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); + + return dataVariables; + } + private cleanupListeners() { this.variableListeners.forEach((listener) => listener.destroy()); this.variableListeners = []; } + + toJSON() { + return { + type: ConditionalVariableType, + condition: this.condition, + ifTrue: this.ifTrue, + ifFalse: this.ifFalse, + }; + } +} +export class MissingConditionError extends Error { + constructor() { + super('No condition was provided to a conditional component.'); + } } diff --git a/packages/core/src/data_sources/model/utils.ts b/packages/core/src/data_sources/model/utils.ts index 44aaf738b5..fe835cb684 100644 --- a/packages/core/src/data_sources/model/utils.ts +++ b/packages/core/src/data_sources/model/utils.ts @@ -1,13 +1,22 @@ import EditorModel from '../../editor/model/Editor'; -import { DataConditionType } from './conditional_variables/DataCondition'; +import { DynamicValue, DynamicValueDefinition } from '../types'; +import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition'; import DataVariable, { DataVariableType } from './DataVariable'; +export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition { + return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value.type); +} + +export function isDynamicValue(value: any): value is DynamicValue { + return value instanceof DataVariable || value instanceof DataCondition; +} + export function isDataVariable(variable: any) { return variable?.type === DataVariableType; } export function isDataCondition(variable: any) { - return variable?.type === DataConditionType; + return variable?.type === ConditionalVariableType; } export function evaluateVariable(variable: any, em: EditorModel) { diff --git a/packages/core/src/data_sources/types.ts b/packages/core/src/data_sources/types.ts index 3b23326e64..f1873fd3ec 100644 --- a/packages/core/src/data_sources/types.ts +++ b/packages/core/src/data_sources/types.ts @@ -1,7 +1,12 @@ import { ObjectAny } from '../common'; +import ComponentDataVariable from './model/ComponentDataVariable'; import DataRecord from './model/DataRecord'; import DataRecords from './model/DataRecords'; +import DataVariable, { DataVariableDefinition } from './model/DataVariable'; +import { ConditionalVariableDefinition, DataCondition } from './model/conditional_variables/DataCondition'; +export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition; +export type DynamicValueDefinition = DataVariableDefinition | ConditionalVariableDefinition; export interface DataRecordProps extends ObjectAny { /** * Record id. diff --git a/packages/core/src/data_sources/view/ComponentDynamicView.ts b/packages/core/src/data_sources/view/ComponentDynamicView.ts new file mode 100644 index 0000000000..75c287d456 --- /dev/null +++ b/packages/core/src/data_sources/view/ComponentDynamicView.ts @@ -0,0 +1,4 @@ +import ComponentView from '../../dom_components/view/ComponentView'; +import ConditionalComponent from '../model/conditional_variables/ConditionalComponent'; + +export default class ConditionalComponentView extends ComponentView {} diff --git a/packages/core/src/dom_components/index.ts b/packages/core/src/dom_components/index.ts index 51b13d17f3..3bdd0ff9ce 100644 --- a/packages/core/src/dom_components/index.ts +++ b/packages/core/src/dom_components/index.ts @@ -125,6 +125,9 @@ import { BlockProperties } from '../block_manager/model/Block'; import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; import { DataVariableType } from '../data_sources/model/DataVariable'; +import { ConditionalVariableType } from '../data_sources/model/conditional_variables/DataCondition'; +import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ConditionalComponent'; +import ConditionalComponentView from '../data_sources/view/ComponentDynamicView'; export type ComponentEvent = | 'component:create' @@ -190,6 +193,11 @@ export interface CanMoveResult { export default class ComponentManager extends ItemManagerModule { componentTypes: ComponentStackItem[] = [ + { + id: ConditionalVariableType, + model: ComponentConditionalVariable, + view: ConditionalComponentView, + }, { id: DataVariableType, model: ComponentDataVariable, diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index 7abb1af35b..fe93d6931d 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -52,6 +52,9 @@ import { updateSymbolProps, } from './SymbolUtils'; import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; +import { ConditionalVariableType, DataCondition } from '../../data_sources/model/conditional_variables/DataCondition'; +import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils'; +import { DynamicValueDefinition } from '../../data_sources/types'; export interface IComponent extends ExtractMethods {} @@ -69,6 +72,7 @@ export const keySymbol = '__symbol'; export const keySymbolOvrd = '__symbol_ovrd'; export const keyUpdate = ComponentsEvents.update; export const keyUpdateInside = ComponentsEvents.updateInside; +export const dynamicAttrKey = 'attributes-dynamic-value'; /** * The Component object represents a single node of our template structure, so when you update its properties the changes are @@ -769,11 +773,26 @@ export default class Component extends StyleableModel { } } - const attrDataVariable = this.get('attributes-data-variable'); + const attrDataVariable = this.get(dynamicAttrKey) as { + [key: string]: TraitDataVariable | DynamicValueDefinition; + }; if (attrDataVariable) { Object.entries(attrDataVariable).forEach(([key, value]) => { - const dataVariable = value instanceof TraitDataVariable ? value : new TraitDataVariable(value, { em }); - attributes[key] = dataVariable.getDataValue(); + let dataVariable: TraitDataVariable | DataCondition; + if (isDynamicValue(value)) { + dataVariable = value; + } else if (isDynamicValueDefinition(value)) { + const type = value.type; + + if (type === ConditionalVariableType) { + const { condition, ifTrue, ifFalse } = value; + dataVariable = new DataCondition(condition, ifTrue, ifFalse, { em }); + } else { + dataVariable = new TraitDataVariable(value, { em }); + } + } + + attributes[key] = dataVariable!.getDataValue(); }); } @@ -915,7 +934,7 @@ export default class Component extends StyleableModel { this.off(event, this.initTraits); this.__loadTraits(); const attrs = { ...this.get('attributes') }; - const traitDataVariableAttr: ObjectAny = {}; + const traitDynamicValueAttr: ObjectAny = {}; const traits = this.traits; traits.each((trait) => { const name = trait.getName(); @@ -928,11 +947,11 @@ export default class Component extends StyleableModel { } if (trait.dynamicVariable) { - traitDataVariableAttr[name] = trait.dynamicVariable; + traitDynamicValueAttr[name] = trait.dynamicVariable; } }); traits.length && this.set('attributes', attrs); - Object.keys(traitDataVariableAttr).length && this.set('attributes-data-variable', traitDataVariableAttr); + Object.keys(traitDynamicValueAttr).length && this.set(dynamicAttrKey, traitDynamicValueAttr); this.on(event, this.initTraits); changed && em && em.trigger('component:toggled'); return this; diff --git a/packages/core/src/domain_abstract/model/StyleableModel.ts b/packages/core/src/domain_abstract/model/StyleableModel.ts index e325d8751c..4e36417281 100644 --- a/packages/core/src/domain_abstract/model/StyleableModel.ts +++ b/packages/core/src/domain_abstract/model/StyleableModel.ts @@ -5,22 +5,19 @@ import Selectors from '../../selector_manager/model/Selectors'; import { shallowDiff } from '../../utils/mixins'; import EditorModel from '../../editor/model/Editor'; import StyleDataVariable from '../../data_sources/model/StyleDataVariable'; -import { DataVariableType } from '../../data_sources/model/DataVariable'; +import { DataVariableDefinition, DataVariableType } from '../../data_sources/model/DataVariable'; import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; import CssRuleView from '../../css_composer/view/CssRuleView'; import ComponentView from '../../dom_components/view/ComponentView'; import Frame from '../../canvas/model/Frame'; - -export type StyleProps = Record< - string, - | string - | string[] - | { - type: typeof DataVariableType; - defaultValue: string; - path: string; - } ->; +import { + DataCondition, + ConditionalVariableType, + ConditionalVariableDefinition, +} from '../../data_sources/model/conditional_variables/DataCondition'; +import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils'; +import { DynamicValueDefinition } from '../../data_sources/types'; +export type StyleProps = Record; export type UpdateStyleOptions = SetOptions & { partial?: boolean; @@ -113,16 +110,8 @@ export default class StyleableModel extends Model } const styleValue = newStyle[key]; - if (typeof styleValue === 'object' && styleValue.type === DataVariableType) { - const dynamicType = styleValue.type; - let styleDynamicVariable; - switch (dynamicType) { - case DataVariableType: - styleDynamicVariable = new StyleDataVariable(styleValue, { em: this.em }); - break; - default: - throw new Error(`Invalid data variable type. Expected '${DataVariableType}', but found '${dynamicType}'.`); - } + if (isDynamicValueDefinition(styleValue)) { + const styleDynamicVariable = this.resolveDynamicValue(styleValue); newStyle[key] = styleDynamicVariable; this.manageDataVariableListener(styleDynamicVariable, key); } @@ -150,10 +139,31 @@ export default class StyleableModel extends Model return newStyle; } + private resolveDynamicValue(styleValue: DynamicValueDefinition) { + const dynamicType = styleValue.type; + let styleDynamicVariable; + switch (dynamicType) { + case DataVariableType: + styleDynamicVariable = new StyleDataVariable(styleValue, { em: this.em }); + break; + case ConditionalVariableType: { + const { condition, ifTrue, ifFalse } = styleValue; + styleDynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em! }); + break; + } + default: + throw new Error( + `Unsupported dynamic value type for styles. Only '${DataVariableType}' and '${ConditionalVariableType}' are supported. Received '${dynamicType}'.`, + ); + } + + return styleDynamicVariable; + } + /** * Manage DataVariableListenerManager for a style property */ - manageDataVariableListener(dataVar: StyleDataVariable, styleProp: string) { + manageDataVariableListener(dataVar: StyleDataVariable | DataCondition, styleProp: string) { if (this.dynamicVariableListeners[styleProp]) { this.dynamicVariableListeners[styleProp].listenToDynamicVariable(); } else { @@ -187,7 +197,7 @@ export default class StyleableModel extends Model } /** - * Resolve data variables to their actual values + * Resolve dynamic values ( datasource variables - conditional variables ) to their actual values */ resolveDataVariables(style: StyleProps): StyleProps { const resolvedStyle = { ...style }; @@ -198,16 +208,12 @@ export default class StyleableModel extends Model return; } - if ( - typeof styleValue === 'object' && - styleValue.type === DataVariableType && - !(styleValue instanceof StyleDataVariable) - ) { - const dataVar = new StyleDataVariable(styleValue, { em: this.em }); + if (isDynamicValueDefinition(styleValue)) { + const dataVar = this.resolveDynamicValue(styleValue); resolvedStyle[key] = dataVar.getDataValue(); } - if (styleValue instanceof StyleDataVariable) { + if (isDynamicValue(styleValue)) { resolvedStyle[key] = styleValue.getDataValue(); } }); diff --git a/packages/core/src/trait_manager/model/Trait.ts b/packages/core/src/trait_manager/model/Trait.ts index 17dd6c7732..185bbce1df 100644 --- a/packages/core/src/trait_manager/model/Trait.ts +++ b/packages/core/src/trait_manager/model/Trait.ts @@ -1,3 +1,4 @@ +import { ConditionalVariableType, DataCondition } from './../../data_sources/model/conditional_variables/DataCondition'; import { isString, isUndefined } from 'underscore'; import Category from '../../abstract/ModuleCategory'; import { LocaleOptions, Model, SetOptions } from '../../common'; @@ -10,6 +11,7 @@ import Traits from './Traits'; import TraitDataVariable from '../../data_sources/model/TraitDataVariable'; import { DataVariableType } from '../../data_sources/model/DataVariable'; import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager'; +import { isDynamicValueDefinition } from '../../data_sources/model/utils'; /** * @property {String} id Trait id, eg. `my-trait-id`. @@ -29,7 +31,7 @@ export default class Trait extends Model { em: EditorModel; view?: TraitView; el?: HTMLElement; - dynamicVariable?: TraitDataVariable; + dynamicVariable?: TraitDataVariable | DataCondition; dynamicVariableListener?: DynamicVariableListenerManager; defaults() { @@ -57,14 +59,19 @@ export default class Trait extends Model { } this.em = em; - if (this.attributes.value && typeof this.attributes.value === 'object') { + if (isDynamicValueDefinition(this.attributes.value)) { const dataType = this.attributes.value.type; switch (dataType) { case DataVariableType: this.dynamicVariable = new TraitDataVariable(this.attributes.value, { em: this.em, trait: this }); break; + case ConditionalVariableType: { + const { condition, ifTrue, ifFalse } = this.attributes.value; + this.dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em }); + break; + } default: - throw new Error(`Invalid data variable type. Expected '${DataVariableType}', but found '${dataType}'.`); + return; } const dv = this.dynamicVariable.getDataValue(); diff --git a/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap b/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap index 0a0d726e00..cdb56af488 100644 --- a/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap +++ b/packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap @@ -126,7 +126,7 @@ exports[`DataSource Serialization .getProjectData TraitDataVariable 1`] = ` "attributes": { "value": "test-value", }, - "attributes-data-variable": { + "attributes-dynamic-value": { "value": { "defaultValue": "default", "path": "test-input.id1.value", diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts new file mode 100644 index 0000000000..efa95e1c35 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts @@ -0,0 +1,242 @@ +import { Component, DataSourceManager, Editor } from '../../../../../src'; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { DataSourceProps } from '../../../../../src/data_sources/types'; +import ConditionalComponentView from '../../../../../src/data_sources/view/ComponentDynamicView'; +import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; +import ComponentTableView from '../../../../../src/dom_components/view/ComponentTableView'; +import ComponentTextView from '../../../../../src/dom_components/view/ComponentTextView'; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { setupTestEditor } from '../../../../common'; + +describe('ComponentConditionalVariable', () => { + let editor: Editor; + let em: EditorModel; + let dsm: DataSourceManager; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + ({ editor, em, dsm, cmpRoot } = setupTestEditor()); + }); + + afterEach(() => { + em.destroy(); + }); + + it('should add a component with a condition that evaluates a component definition', () => { + const component = cmpRoot.append({ + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'some text', + }, + })[0]; + expect(component).toBeDefined(); + expect(component.get('type')).toBe(ConditionalVariableType); + expect(component.getInnerHTML()).toBe('

some text

'); + const componentView = component.getView(); + expect(componentView).toBeInstanceOf(ConditionalComponentView); + expect(componentView?.el.textContent).toBe('some text'); + + const childComponent = getFirstChild(component); + const childView = getFirstChildView(component); + expect(childComponent).toBeDefined(); + expect(childComponent.get('type')).toBe('text'); + expect(childComponent.getInnerHTML()).toBe('some text'); + expect(childView).toBeInstanceOf(ComponentTextView); + expect(childView?.el.innerHTML).toBe('some text'); + }); + + it('should add a component with a condition that evaluates a string', () => { + const component = cmpRoot.append({ + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: '

some text

', + })[0]; + expect(component).toBeDefined(); + expect(component.get('type')).toBe(ConditionalVariableType); + expect(component.getInnerHTML()).toBe('

some text

'); + const componentView = component.getView(); + expect(componentView).toBeInstanceOf(ConditionalComponentView); + expect(componentView?.el.textContent).toBe('some text'); + + const childComponent = getFirstChild(component); + const childView = getFirstChildView(component); + expect(childComponent).toBeDefined(); + expect(childComponent.get('type')).toBe('text'); + expect(childComponent.getInnerHTML()).toBe('some text'); + expect(childView).toBeInstanceOf(ComponentTextView); + expect(childView?.el.innerHTML).toBe('some text'); + }); + + it('should test component variable with data-source', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'left_id', left: 'Name1' }, + { id: 'right_id', right: 'Name1' }, + ], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'Some value', + }, + ifFalse: { + tagName: 'h1', + type: 'text', + content: 'False value', + }, + })[0]; + + const childComponent = getFirstChild(component); + expect(childComponent).toBeDefined(); + expect(childComponent.get('type')).toBe('text'); + expect(childComponent.getInnerHTML()).toBe('Some value'); + + /* Test changing datasources */ + updatedsmLeftValue(dsm, 'Diffirent value'); + expect(getFirstChild(component).getInnerHTML()).toBe('False value'); + expect(getFirstChildView(component)?.el.innerHTML).toBe('False value'); + updatedsmLeftValue(dsm, 'Name1'); + expect(getFirstChild(component).getInnerHTML()).toBe('Some value'); + expect(getFirstChildView(component)?.el.innerHTML).toBe('Some value'); + }); + + it('should test a conditional component with a child that is also a conditional component', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'left_id', left: 'Name1' }, + { id: 'right_id', right: 'Name1' }, + ], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: { + tagName: 'div', + components: [ + { + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: { + tagName: 'table', + type: 'table', + }, + }, + ], + }, + })[0]; + + const innerComponent = getFirstChild(getFirstChild(component)); + const innerComponentView = getFirstChildView(innerComponent); + const innerHTML = '
'; + expect(innerComponent.getInnerHTML()).toBe(innerHTML); + expect(innerComponentView).toBeInstanceOf(ComponentTableView); + expect(innerComponentView?.el.tagName).toBe('TABLE'); + }); + + it('should store conditional components', () => { + const conditionalCmptDef = { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: [ + { + tagName: 'h1', + type: 'text', + content: 'some text', + }, + ], + }; + + cmpRoot.append(conditionalCmptDef)[0]; + + const projectData = editor.getProjectData(); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const storageCmptDef = frame.component.components[0]; + expect(storageCmptDef).toEqual(conditionalCmptDef); + }); + + it('should throw an error if no condition is passed', () => { + const conditionalCmptDef = { + type: ConditionalVariableType, + ifTrue: { + tagName: 'h1', + type: 'text', + content: 'some text', + }, + }; + + expect(() => { + cmpRoot.append(conditionalCmptDef); + }).toThrow(MissingConditionError); + }); +}); + +function updatedsmLeftValue(dsm: DataSourceManager, newValue: string) { + dsm.get('ds1').getRecord('left_id')?.set('left', newValue); +} + +function getFirstChildView(component: Component) { + return getFirstChild(component).getView(); +} + +function getFirstChild(component: Component) { + return component.components().at(0); +} diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts new file mode 100644 index 0000000000..ad9bd4ef25 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts @@ -0,0 +1,135 @@ +import { DataSourceManager, Editor } from '../../../../../src'; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import { + ConditionalVariableType, + MissingConditionError, +} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { DataSourceProps } from '../../../../../src/data_sources/types'; +import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { filterObjectForSnapshot, setupTestEditor } from '../../../../common'; + +describe('StyleConditionalVariable', () => { + let editor: Editor; + let em: EditorModel; + let dsm: DataSourceManager; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + ({ editor, em, dsm, cmpRoot } = setupTestEditor()); + }); + + afterEach(() => { + em.destroy(); + }); + + it('should add a component with a conditionally styled attribute', () => { + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'red', + ifFalse: 'black', + }, + }, + })[0]; + + expect(component).toBeDefined(); + expect(component.getStyle().color).toBe('red'); + }); + + it('should change style based on data source changes', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [ + { id: 'left_id', left: 'Value1' }, + { id: 'right_id', right: 'Value2' }, + ], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: { + type: DataVariableType, + path: 'ds1.right_id.right', + }, + }, + ifTrue: 'green', + ifFalse: 'blue', + }, + }, + })[0]; + + expect(component.getStyle().color).toBe('blue'); + + dsm.get('ds1').getRecord('right_id')?.set('right', 'Value1'); + expect(component.getStyle().color).toBe('green'); + }); + + it('should throw an error if no condition is passed in style', () => { + expect(() => { + cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + ifTrue: 'grey', + ifFalse: 'red', + }, + }, + }); + }).toThrow(MissingConditionError); + }); + + it.skip('should store components with conditional styles correctly', () => { + const conditionalStyleDef = { + tagName: 'h1', + type: 'text', + content: 'some text', + style: { + color: { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'yellow', + ifFalse: 'black', + }, + }, + }; + + cmpRoot.append(conditionalStyleDef)[0]; + + const projectData = filterObjectForSnapshot(editor.getProjectData()); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const storedComponent = frame.component.components[0]; + expect(storedComponent).toEqual(expect.objectContaining(conditionalStyleDef)); + }); +}); diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts new file mode 100644 index 0000000000..8e109aac6c --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts @@ -0,0 +1,283 @@ +import { DataSourceManager, Editor } from '../../../../../src'; +import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; +import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; +import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; +import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; +import { DataSourceProps } from '../../../../../src/data_sources/types'; +import Component, { dynamicAttrKey } from '../../../../../src/dom_components/model/Component'; +import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; +import EditorModel from '../../../../../src/editor/model/Editor'; +import { filterObjectForSnapshot, setupTestEditor } from '../../../../common'; + +describe('TraitConditionalVariable', () => { + let editor: Editor; + let em: EditorModel; + let dsm: DataSourceManager; + let cmpRoot: ComponentWrapper; + + beforeEach(() => { + ({ editor, em, dsm, cmpRoot } = setupTestEditor()); + }); + + afterEach(() => { + em.destroy(); + }); + + it('should add a trait with a condition evaluating to a string', () => { + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'title', + value: { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'Some title', + }, + }, + ], + })[0]; + + testComponentAttr(component, 'title', 'Some title'); + }); + + it('should add a trait with a data-source condition', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [{ id: 'left_id', left: 'Name1' }], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'title', + value: { + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: 'Name1', + }, + ifTrue: 'Valid name', + ifFalse: 'Invalid name', + }, + }, + ], + })[0]; + + testComponentAttr(component, 'title', 'Valid name'); + }); + + it('should change trait value with changing data-source value', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [{ id: 'left_id', left: 'Name1' }], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'title', + value: { + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: 'Name1', + }, + ifTrue: 'Correct name', + ifFalse: 'Incorrect name', + }, + }, + ], + })[0]; + + testComponentAttr(component, 'title', 'Correct name'); + dsm.get('ds1').getRecord('left_id')?.set('left', 'Different name'); + testComponentAttr(component, 'title', 'Incorrect name'); + }); + + it('should throw an error if no condition is passed in trait', () => { + expect(() => { + cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'invalidTrait', + value: { + type: ConditionalVariableType, + }, + }, + ], + }); + }).toThrow(MissingConditionError); + }); + + it('should store traits with conditional values correctly', () => { + const conditionalTrait = { + type: ConditionalVariableType, + condition: { + left: 0, + operator: NumberOperation.greaterThan, + right: -1, + }, + ifTrue: 'Positive', + }; + cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'dynamicTrait', + value: conditionalTrait, + }, + ], + })[0]; + + const projectData = editor.getProjectData(); + const snapshot = filterObjectForSnapshot(projectData); + expect(snapshot).toMatchSnapshot(``); + const page = projectData.pages[0]; + const frame = page.frames[0]; + const storedComponent = frame.component.components[0]; + + expect(storedComponent[dynamicAttrKey]).toEqual({ + dynamicTrait: conditionalTrait, + }); + }); + + it('should load traits with conditional values correctly', () => { + const projectData = { + pages: [ + { + frames: [ + { + component: { + components: [ + { + attributes: { + dynamicTrait: 'Default', + }, + [dynamicAttrKey]: { + dynamicTrait: { + condition: { + left: 0, + operator: '>', + right: -1, + }, + ifTrue: 'Positive', + type: 'conditional-variable', + }, + }, + type: 'text', + }, + ], + type: 'wrapper', + }, + }, + ], + type: 'main', + }, + ], + }; + + editor.loadProjectData(projectData); + const components = editor.getComponents(); + const component = components.models[0]; + expect(component.getAttributes()).toEqual({ dynamicTrait: 'Positive' }); + }); + + it('should be property on the component with `changeProp:true`', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [{ id: 'left_id', left: 'Name1' }], + }; + dsm.add(dataSource); + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'title', + changeProp: true, + value: { + type: ConditionalVariableType, + condition: { + left: { + type: DataVariableType, + path: 'ds1.left_id.left', + }, + operator: GenericOperation.equals, + right: 'Name1', + }, + ifTrue: 'Correct name', + ifFalse: 'Incorrect name', + }, + }, + ], + })[0]; + + // TODO: make dynamic values not to change the attributes if `changeProp:true` + // expect(component.getView()?.el.getAttribute('title')).toBeNull(); + expect(component.get('title')).toBe('Correct name'); + + dsm.get('ds1').getRecord('left_id')?.set('left', 'Different name'); + // expect(component.getView()?.el.getAttribute('title')).toBeNull(); + expect(component.get('title')).toBe('Incorrect name'); + }); + + it('should handle objects as traits (other than dynamic values)', () => { + const traitValue = { + type: 'UNKNOWN_TYPE', + condition: "This's not a condition", + value: 'random value', + }; + + const component = cmpRoot.append({ + tagName: 'h1', + type: 'text', + traits: [ + { + type: 'text', + name: 'title', + value: traitValue, + }, + ], + })[0]; + expect(component.getTrait('title').get('value')).toEqual(traitValue); + expect(component.getAttributes().title).toEqual(traitValue); + }); +}); + +function testComponentAttr(component: Component, trait: string, value: string) { + expect(component).toBeDefined(); + expect(component.getTrait(trait).get('value')).toBe(value); + expect(component.getAttributes()[trait]).toBe(value); + expect(component.getView()?.el.getAttribute(trait)).toBe(value); +} diff --git a/packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap b/packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap new file mode 100644 index 0000000000..34066fa8c1 --- /dev/null +++ b/packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TraitConditionalVariable should store traits with conditional values correctly 1`] = ` +{ + "assets": [], + "dataSources": [], + "pages": [ + { + "frames": [ + { + "component": { + "components": [ + { + "attributes": { + "dynamicTrait": "Positive", + }, + "attributes-dynamic-value": { + "dynamicTrait": { + "condition": { + "left": 0, + "operator": ">", + "right": -1, + }, + "ifTrue": "Positive", + "type": "conditional-variable", + }, + }, + "tagName": "h1", + "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": "data-variable-id", + }, + ], + "id": "data-variable-id", + "type": "main", + }, + ], + "styles": [], + "symbols": [], +} +`; diff --git a/packages/core/test/specs/data_sources/serialization.ts b/packages/core/test/specs/data_sources/serialization.ts index 9a9c463a11..26d95f56d1 100644 --- a/packages/core/test/specs/data_sources/serialization.ts +++ b/packages/core/test/specs/data_sources/serialization.ts @@ -6,6 +6,7 @@ import EditorModel from '../../../src/editor/model/Editor'; import { ProjectData } from '../../../src/storage_manager'; import { DataSourceProps } from '../../../src/data_sources/types'; import { filterObjectForSnapshot, setupTestEditor } from '../../common'; +import { dynamicAttrKey } from '../../../src/dom_components/model/Component'; describe('DataSource Serialization', () => { let editor: Editor; @@ -143,8 +144,8 @@ describe('DataSource Serialization', () => { const page = projectData.pages[0]; const frame = page.frames[0]; const component = frame.component.components[0]; - expect(component).toHaveProperty('attributes-data-variable'); - expect(component['attributes-data-variable']).toEqual({ + expect(component).toHaveProperty(dynamicAttrKey); + expect(component[dynamicAttrKey]).toEqual({ value: dataVariable, }); expect(component.attributes).toEqual({ @@ -297,7 +298,7 @@ describe('DataSource Serialization', () => { attributes: { value: 'default', }, - 'attributes-data-variable': { + [dynamicAttrKey]: { value: { path: 'test-input.id1.value', type: 'data-variable',