Browse Source

Merge branch 'dev' into fix-page-clone

pull/6291/head
mohamed yahia 1 year ago
committed by GitHub
parent
commit
66917aa91a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 7
      packages/core/src/data_sources/model/ComponentDataVariable.ts
  2. 7
      packages/core/src/data_sources/model/DataVariable.ts
  3. 19
      packages/core/src/data_sources/model/DataVariableListenerManager.ts
  4. 7
      packages/core/src/data_sources/model/TraitDataVariable.ts
  5. 12
      packages/core/src/data_sources/model/conditional_variables/Condition.ts
  6. 46
      packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts
  7. 73
      packages/core/src/data_sources/model/conditional_variables/DataCondition.ts
  8. 13
      packages/core/src/data_sources/model/utils.ts
  9. 5
      packages/core/src/data_sources/types.ts
  10. 4
      packages/core/src/data_sources/view/ComponentDynamicView.ts
  11. 8
      packages/core/src/dom_components/index.ts
  12. 31
      packages/core/src/dom_components/model/Component.ts
  13. 68
      packages/core/src/domain_abstract/model/StyleableModel.ts
  14. 13
      packages/core/src/trait_manager/model/Trait.ts
  15. 2
      packages/core/test/specs/data_sources/__snapshots__/serialization.ts.snap
  16. 242
      packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts
  17. 135
      packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts
  18. 283
      packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts
  19. 59
      packages/core/test/specs/data_sources/model/conditional_variables/__snapshots__/ConditionalTraits.ts.snap
  20. 7
      packages/core/test/specs/data_sources/serialization.ts

7
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) {

7
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();

19
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;

7
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;
}

12
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);

46
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();
}
}

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

13
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) {

5
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.

4
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<ConditionalComponent> {}

8
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<DomComponentsConfig, any> {
componentTypes: ComponentStackItem[] = [
{
id: ConditionalVariableType,
model: ComponentConditionalVariable,
view: ConditionalComponentView,
},
{
id: DataVariableType,
model: ComponentDataVariable,

31
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<Component> {}
@ -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<ComponentProperties> {
}
}
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<ComponentProperties> {
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<ComponentProperties> {
}
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;

68
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<string, string | string[] | DataVariableDefinition | ConditionalVariableDefinition>;
export type UpdateStyleOptions = SetOptions & {
partial?: boolean;
@ -113,16 +110,8 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
}
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<T extends ObjectHash = any> extends Model<T>
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<T extends ObjectHash = any> extends Model<T>
}
/**
* 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<T extends ObjectHash = any> extends Model<T>
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();
}
});

13
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<TraitProperties> {
em: EditorModel;
view?: TraitView;
el?: HTMLElement;
dynamicVariable?: TraitDataVariable;
dynamicVariable?: TraitDataVariable | DataCondition;
dynamicVariableListener?: DynamicVariableListenerManager;
defaults() {
@ -57,14 +59,19 @@ export default class Trait extends Model<TraitProperties> {
}
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();

2
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",

242
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('<h1>some text</h1>');
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: '<h1>some text</h1>',
})[0];
expect(component).toBeDefined();
expect(component.get('type')).toBe(ConditionalVariableType);
expect(component.getInnerHTML()).toBe('<h1>some text</h1>');
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 = '<table><tbody><tr class="row"><td class="cell"></td></tr></tbody></table>';
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);
}

135
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));
});
});

283
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);
}

59
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": [],
}
`;

7
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',

Loading…
Cancel
Save