Browse Source

Add collection components (#6359)

pull/6398/head
mohamed yahia 1 year ago
committed by GitHub
parent
commit
d629989499
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      README.md
  2. 2
      packages/core/src/css_composer/model/CssRule.ts
  3. 14
      packages/core/src/data_sources/model/ComponentDataVariable.ts
  4. 96
      packages/core/src/data_sources/model/DataResolverListener.ts
  5. 2
      packages/core/src/data_sources/model/DataSource.ts
  6. 36
      packages/core/src/data_sources/model/DataVariable.ts
  7. 88
      packages/core/src/data_sources/model/DataVariableListenerManager.ts
  8. 9
      packages/core/src/data_sources/model/StyleDataVariable.ts
  9. 8
      packages/core/src/data_sources/model/TraitDataVariable.ts
  10. 36
      packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts
  11. 27
      packages/core/src/data_sources/model/conditional_variables/Condition.ts
  12. 46
      packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts
  13. 80
      packages/core/src/data_sources/model/conditional_variables/DataCondition.ts
  14. 7
      packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts
  15. 2
      packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts
  16. 284
      packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts
  17. 53
      packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts
  18. 158
      packages/core/src/data_sources/model/data_collection/DataCollectionVariable.ts
  19. 5
      packages/core/src/data_sources/model/data_collection/constants.ts
  20. 57
      packages/core/src/data_sources/model/data_collection/types.ts
  21. 60
      packages/core/src/data_sources/model/utils.ts
  22. 19
      packages/core/src/data_sources/types.ts
  23. 21
      packages/core/src/data_sources/view/ComponentDataCollectionVariableView.ts
  24. 4
      packages/core/src/data_sources/view/ComponentDataCollectionView.ts
  25. 4
      packages/core/src/data_sources/view/ComponentDataConditionView.ts
  26. 21
      packages/core/src/data_sources/view/ComponentDataVariableView.ts
  27. 4
      packages/core/src/data_sources/view/ComponentDynamicView.ts
  28. 29
      packages/core/src/dom_components/index.ts
  29. 96
      packages/core/src/dom_components/model/Component.ts
  30. 116
      packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts
  31. 66
      packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts
  32. 180
      packages/core/src/dom_components/model/ComponentResolverWatcher.ts
  33. 117
      packages/core/src/dom_components/model/DynamicValueWatcher.ts
  34. 78
      packages/core/src/dom_components/model/SymbolUtils.ts
  35. 6
      packages/core/src/dom_components/model/types.ts
  36. 102
      packages/core/src/domain_abstract/model/StyleableModel.ts
  37. 5
      packages/core/src/trait_manager/model/Trait.ts
  38. 2
      packages/core/src/utils/mixins.ts
  39. 31
      packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts
  40. 11
      packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts
  41. 10
      packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts
  42. 39
      packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts
  43. 1004
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts
  44. 264
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionVariable.ts
  45. 519
      packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap
  46. 141
      packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionVariable.ts.snap
  47. 36
      packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap
  48. 430
      packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts

2
README.md

@ -1 +1 @@
./packages/core/README.md ./packages/core/README.md

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

@ -7,7 +7,6 @@ import { isEmptyObj, hasWin } from '../../utils/mixins';
import Selector, { SelectorProps } from '../../selector_manager/model/Selector'; import Selector, { SelectorProps } from '../../selector_manager/model/Selector';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import CssRuleView from '../view/CssRuleView'; import CssRuleView from '../view/CssRuleView';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
/** @private */ /** @private */
export interface CssRuleProperties { export interface CssRuleProperties {
@ -95,7 +94,6 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
em?: EditorModel; em?: EditorModel;
opt: any; opt: any;
views: CssRuleView[] = []; views: CssRuleView[] = [];
dynamicVariableListeners: Record<string, DynamicVariableListenerManager> = {};
defaults() { defaults() {
return { return {

14
packages/core/src/data_sources/model/ComponentDataVariable.ts

@ -1,8 +1,11 @@
import Component from '../../dom_components/model/Component'; import Component from '../../dom_components/model/Component';
import { ComponentOptions } from '../../dom_components/model/types';
import { toLowerCase } from '../../utils/mixins'; import { toLowerCase } from '../../utils/mixins';
import { DataVariableType } from './DataVariable'; import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable';
export default class ComponentDataVariable extends Component { export default class ComponentDataVariable extends Component {
dataResolver: DataVariable;
get defaults() { get defaults() {
return { return {
// @ts-ignore // @ts-ignore
@ -13,9 +16,14 @@ export default class ComponentDataVariable extends Component {
}; };
} }
constructor(props: DataVariableProps, opt: ComponentOptions) {
super(props, opt);
const { type, path, defaultValue } = props;
this.dataResolver = new DataVariable({ type, path, defaultValue }, opt);
}
getDataValue() { getDataValue() {
const { path, defaultValue } = this.attributes; return this.dataResolver.getDataValue();
return this.em.DataSources.getValue(path, defaultValue);
} }
getInnerHTML() { getInnerHTML() {

96
packages/core/src/data_sources/model/DataResolverListener.ts

@ -0,0 +1,96 @@
import { DataSourcesEvents, DataSourceListener } from '../types';
import { stringToPath } from '../../utils/mixins';
import { Model } from '../../common';
import EditorModel from '../../editor/model/Editor';
import DataVariable, { DataVariableType } from './DataVariable';
import { DataResolver } from '../types';
import { DataCondition, DataConditionType } from './conditional_variables/DataCondition';
import { DataCollectionVariableType } from './data_collection/constants';
import DataCollectionVariable from './data_collection/DataCollectionVariable';
export interface DataResolverListenerProps {
em: EditorModel;
resolver: DataResolver;
onUpdate: (value: any) => void;
}
export default class DataResolverListener {
private listeners: DataSourceListener[] = [];
private em: EditorModel;
private onUpdate: (value: any) => void;
private model = new Model();
resolver: DataResolver;
constructor(props: DataResolverListenerProps) {
this.em = props.em;
this.resolver = props.resolver;
this.onUpdate = props.onUpdate;
this.listenToResolver();
}
private onChange = () => {
const value = this.resolver.getDataValue();
this.onUpdate(value);
};
listenToResolver() {
const { resolver, model } = this;
this.removeListeners();
let listeners: DataSourceListener[] = [];
const type = resolver.attributes.type;
switch (type) {
case DataCollectionVariableType:
listeners = this.listenToDataCollectionVariable(resolver as DataCollectionVariable);
break;
case DataVariableType:
listeners = this.listenToDataVariable(resolver as DataVariable);
break;
case DataConditionType:
listeners = this.listenToConditionalVariable(resolver as DataCondition);
break;
}
listeners.forEach((ls) => model.listenTo(ls.obj, ls.event, this.onChange));
this.listeners = listeners;
}
private listenToConditionalVariable(dataVariable: DataCondition) {
const { em } = this;
const dataListeners = dataVariable.getDependentDataVariables().flatMap((dataVariable) => {
return this.listenToDataVariable(new DataVariable(dataVariable, { em }));
});
return dataListeners;
}
private listenToDataVariable(dataVariable: DataVariable) {
const { em } = this;
const dataListeners: DataSourceListener[] = [];
const { path } = dataVariable.attributes;
const normPath = stringToPath(path || '').join('.');
const [ds, dr] = em.DataSources.fromPath(path!);
ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' });
dr && dataListeners.push({ obj: dr, event: 'change' });
dataListeners.push(
{ obj: dataVariable, event: 'change:path change:defaultValue' },
{ obj: em.DataSources.all, event: 'add remove reset' },
{ obj: em, event: `${DataSourcesEvents.path}:${normPath}` },
);
return dataListeners;
}
private listenToDataCollectionVariable(dataVariable: DataCollectionVariable) {
return [{ obj: dataVariable, event: 'change:value' }];
}
private removeListeners() {
this.listeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange));
this.listeners = [];
}
destroy() {
this.removeListeners();
}
}

2
packages/core/src/data_sources/model/DataSource.ts

@ -31,7 +31,7 @@
import { AddOptions, collectionEvents, CombinedModelConstructorOptions, Model, RemoveOptions } from '../../common'; import { AddOptions, collectionEvents, CombinedModelConstructorOptions, Model, RemoveOptions } from '../../common';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import { DataSourceTransformers, DataSourceType, DataSourceProps, RecordPropsType, DataRecordProps } from '../types'; import { DataSourceTransformers, DataSourceType, DataSourceProps, DataRecordProps } from '../types';
import DataRecord from './DataRecord'; import DataRecord from './DataRecord';
import DataRecords from './DataRecords'; import DataRecords from './DataRecords';
import DataSources from './DataSources'; import DataSources from './DataSources';

36
packages/core/src/data_sources/model/DataVariable.ts

@ -1,15 +1,15 @@
import { Model } from '../../common'; import { Model } from '../../common';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import { stringToPath } from '../../utils/mixins';
export const DataVariableType = 'data-variable'; export const DataVariableType = 'data-variable' as const;
export type DataVariableDefinition = {
export interface DataVariableProps {
type: typeof DataVariableType; type: typeof DataVariableType;
path: string; path: string;
defaultValue?: string; defaultValue?: string;
}; }
export default class DataVariable extends Model { export default class DataVariable extends Model<DataVariableProps> {
em?: EditorModel; em?: EditorModel;
defaults() { defaults() {
@ -20,33 +20,13 @@ export default class DataVariable extends Model {
}; };
} }
constructor(attrs: DataVariableDefinition, options: any) { constructor(props: DataVariableProps, options: { em?: EditorModel }) {
super(attrs, options); super(props, options);
this.em = options.em; this.em = options.em;
this.listenToDataSource();
}
listenToDataSource() {
const { path } = this.attributes;
const resolvedPath = stringToPath(path).join('.');
if (this.em) {
this.listenTo(this.em.DataSources, `change:${resolvedPath}`, this.onDataSourceChange);
}
}
onDataSourceChange() {
const newValue = this.getDataValue();
this.set({ value: newValue });
} }
getDataValue() { getDataValue() {
const { path, defaultValue } = this.attributes; const { path, defaultValue } = this.attributes;
if (!this.em) { return this.em?.DataSources.getValue(path!, defaultValue);
throw new Error('EditorModel instance is not provided for a data variable.');
}
const val = this.em?.DataSources.getValue(path, defaultValue);
return val;
} }
} }

88
packages/core/src/data_sources/model/DataVariableListenerManager.ts

@ -1,88 +0,0 @@
import { DataSourcesEvents, DataVariableListener } from '../types';
import { stringToPath } from '../../utils/mixins';
import { Model } from '../../common';
import EditorModel from '../../editor/model/Editor';
import DataVariable, { DataVariableType } from './DataVariable';
import { DynamicValue } from '../types';
import { DataCondition, ConditionalVariableType } from './conditional_variables/DataCondition';
import ComponentDataVariable from './ComponentDataVariable';
export interface DynamicVariableListenerManagerOptions {
em: EditorModel;
dataVariable: DynamicValue;
updateValueFromDataVariable: (value: any) => void;
}
export default class DynamicVariableListenerManager {
private dataListeners: DataVariableListener[] = [];
private em: EditorModel;
dynamicVariable: DynamicValue;
private updateValueFromDynamicVariable: (value: any) => void;
private model = new Model();
constructor(options: DynamicVariableListenerManagerOptions) {
this.em = options.em;
this.dynamicVariable = options.dataVariable;
this.updateValueFromDynamicVariable = options.updateValueFromDataVariable;
this.listenToDynamicVariable();
}
private onChange = () => {
const value = this.dynamicVariable.getDataValue();
this.updateValueFromDynamicVariable(value);
};
listenToDynamicVariable() {
const { em, dynamicVariable } = this;
this.removeListeners();
// @ts-ignore
const type = dynamicVariable.get('type');
let dataListeners: DataVariableListener[] = [];
switch (type) {
case DataVariableType:
dataListeners = this.listenToDataVariable(dynamicVariable as DataVariable | ComponentDataVariable, em);
break;
case ConditionalVariableType:
dataListeners = this.listenToConditionalVariable(dynamicVariable as DataCondition, em);
break;
}
dataListeners.forEach((ls) => this.model.listenTo(ls.obj, ls.event, this.onChange));
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;
const normPath = stringToPath(path || '').join('.');
const [ds, dr] = this.em.DataSources.fromPath(path);
ds && dataListeners.push({ obj: ds.records, event: 'add remove reset' });
dr && dataListeners.push({ obj: dr, event: 'change' });
dataListeners.push(
{ obj: dataVariable, event: 'change:path change:defaultValue' },
{ obj: em.DataSources.all, event: 'add remove reset' },
{ obj: em, event: `${DataSourcesEvents.path}:${normPath}` },
);
return dataListeners;
}
private removeListeners() {
this.dataListeners.forEach((ls) => this.model.stopListening(ls.obj, ls.event, this.onChange));
this.dataListeners = [];
}
destroy() {
this.removeListeners();
}
}

9
packages/core/src/data_sources/model/StyleDataVariable.ts

@ -1,9 +0,0 @@
import DataVariable from './DataVariable';
export default class StyleDataVariable extends DataVariable {
defaults() {
return {
...super.defaults(),
};
}
}

8
packages/core/src/data_sources/model/TraitDataVariable.ts

@ -1,14 +1,14 @@
import DataVariable, { DataVariableDefinition } from './DataVariable'; import DataVariable, { DataVariableProps } from './DataVariable';
import Trait from '../../trait_manager/model/Trait'; import Trait from '../../trait_manager/model/Trait';
import { TraitProperties } from '../../trait_manager/types'; import { TraitProperties } from '../../trait_manager/types';
export type TraitDataVariableDefinition = TraitProperties & DataVariableDefinition; export interface TraitDataVariableProps extends Omit<TraitProperties, 'type'>, DataVariableProps {}
export default class TraitDataVariable extends DataVariable { export default class TraitDataVariable extends DataVariable {
trait?: Trait; trait?: Trait;
constructor(attrs: TraitDataVariableDefinition, options: any) { constructor(props: TraitDataVariableProps, options: any) {
super(attrs, options); super(props, options);
this.trait = options.trait; this.trait = options.trait;
} }

36
packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts

@ -0,0 +1,36 @@
import Component from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types';
import { toLowerCase } from '../../../utils/mixins';
import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition';
export default class ComponentDataCondition extends Component {
dataResolver: DataCondition;
constructor(props: DataConditionProps, opt: ComponentOptions) {
const { condition, ifTrue, ifFalse } = props;
const dataConditionInstance = new DataCondition(condition, ifTrue, ifFalse, { em: opt.em });
super(
{
...props,
type: DataConditionType,
components: dataConditionInstance.getDataValue(),
},
opt,
);
this.dataResolver = dataConditionInstance;
this.dataResolver.onValueChange = this.handleConditionChange.bind(this);
}
private handleConditionChange() {
this.dataResolver.reevaluate();
this.components(this.dataResolver.getDataValue());
}
static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataConditionType;
}
toJSON(): ComponentDefinition {
return this.dataResolver.toJSON();
}
}

27
packages/core/src/data_sources/model/conditional_variables/Condition.ts

@ -1,7 +1,7 @@
import { DataVariableDefinition, DataVariableType } from './../DataVariable'; import { DataVariableProps } from './../DataVariable';
import EditorModel from '../../../editor/model/Editor'; import EditorModel from '../../../editor/model/Editor';
import { evaluateVariable, isDataVariable } from '../utils'; import { evaluateVariable, isDataVariable } from '../utils';
import { ExpressionDefinition, LogicGroupDefinition } from './DataCondition'; import { ExpressionProps, LogicGroupProps } from './DataCondition';
import { LogicalGroupStatement } from './LogicalGroupStatement'; import { LogicalGroupStatement } from './LogicalGroupStatement';
import { Operator } from './operators'; import { Operator } from './operators';
import { GenericOperation, GenericOperator } from './operators/GenericOperator'; import { GenericOperation, GenericOperator } from './operators/GenericOperator';
@ -10,13 +10,15 @@ import { NumberOperator, NumberOperation } from './operators/NumberOperator';
import { StringOperator, StringOperation } from './operators/StringOperations'; import { StringOperator, StringOperation } from './operators/StringOperations';
import { Model } from '../../../common'; import { Model } from '../../../common';
export type ConditionProps = ExpressionProps | LogicGroupProps | boolean;
export class Condition extends Model { export class Condition extends Model {
private condition: ExpressionDefinition | LogicGroupDefinition | boolean; private condition: ConditionProps;
private em: EditorModel; private em: EditorModel;
constructor(condition: ExpressionDefinition | LogicGroupDefinition | boolean, opts: { em: EditorModel }) { constructor(props: ConditionProps, opts: { em: EditorModel }) {
super(condition); super(props);
this.condition = condition; this.condition = props;
this.em = opts.em; this.em = opts.em;
} }
@ -27,7 +29,7 @@ export class Condition extends Model {
/** /**
* Recursively evaluates conditions and logic groups. * Recursively evaluates conditions and logic groups.
*/ */
private evaluateCondition(condition: any): boolean { private evaluateCondition(condition: ConditionProps): boolean {
if (typeof condition === 'boolean') return condition; if (typeof condition === 'boolean') return condition;
if (this.isLogicGroup(condition)) { if (this.isLogicGroup(condition)) {
@ -68,7 +70,7 @@ export class Condition extends Model {
* Extracts all data variables from the condition, including nested ones. * Extracts all data variables from the condition, including nested ones.
*/ */
getDataVariables() { getDataVariables() {
const variables: DataVariableDefinition[] = []; const variables: DataVariableProps[] = [];
this.extractVariables(this.condition, variables); this.extractVariables(this.condition, variables);
return variables; return variables;
} }
@ -76,10 +78,7 @@ export class Condition extends Model {
/** /**
* Recursively extracts variables from expressions or logic groups. * Recursively extracts variables from expressions or logic groups.
*/ */
private extractVariables( private extractVariables(condition: ConditionProps, variables: DataVariableProps[]): void {
condition: boolean | LogicGroupDefinition | ExpressionDefinition,
variables: DataVariableDefinition[],
): void {
if (this.isExpression(condition)) { if (this.isExpression(condition)) {
if (isDataVariable(condition.left)) variables.push(condition.left); if (isDataVariable(condition.left)) variables.push(condition.left);
if (isDataVariable(condition.right)) variables.push(condition.right); if (isDataVariable(condition.right)) variables.push(condition.right);
@ -91,14 +90,14 @@ export class Condition extends Model {
/** /**
* Checks if a condition is a LogicGroup. * Checks if a condition is a LogicGroup.
*/ */
private isLogicGroup(condition: any): condition is LogicGroupDefinition { private isLogicGroup(condition: any): condition is LogicGroupProps {
return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements); return condition && typeof condition.logicalOperator !== 'undefined' && Array.isArray(condition.statements);
} }
/** /**
* Checks if a condition is an Expression. * Checks if a condition is an Expression.
*/ */
private isExpression(condition: any): condition is ExpressionDefinition { private isExpression(condition: any): condition is ExpressionProps {
return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string'; return condition && typeof condition.left !== 'undefined' && typeof condition.operator === 'string';
} }

46
packages/core/src/data_sources/model/conditional_variables/ConditionalComponent.ts

@ -1,46 +0,0 @@
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, ExpressionDefinition, LogicGroupDefinition } from './DataCondition';
type ConditionalComponentDefinition = {
condition: ExpressionDefinition | LogicGroupDefinition | 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();
}
}

80
packages/core/src/data_sources/model/conditional_variables/DataCondition.ts

@ -1,49 +1,46 @@
import { NumberOperation } from './operators/NumberOperator';
import { StringOperation } from './operators/StringOperations';
import { GenericOperation } from './operators/GenericOperator';
import { Model } from '../../../common'; import { Model } from '../../../common';
import { LogicalOperation } from './operators/LogicalOperator';
import DynamicVariableListenerManager from '../DataVariableListenerManager';
import EditorModel from '../../../editor/model/Editor'; import EditorModel from '../../../editor/model/Editor';
import { Condition } from './Condition'; import DataVariable, { DataVariableProps } from '../DataVariable';
import DataVariable, { DataVariableDefinition } from '../DataVariable'; import DataResolverListener from '../DataResolverListener';
import { evaluateVariable, isDataVariable } from '../utils'; import { evaluateVariable, isDataVariable } from '../utils';
import { Condition, ConditionProps } from './Condition';
import { GenericOperation } from './operators/GenericOperator';
import { LogicalOperation } from './operators/LogicalOperator';
import { NumberOperation } from './operators/NumberOperator';
import { StringOperation } from './operators/StringOperations';
export const DataConditionType = 'data-condition';
export const ConditionalVariableType = 'conditional-variable'; export interface ExpressionProps {
export type ExpressionDefinition = {
left: any; left: any;
operator: GenericOperation | StringOperation | NumberOperation; operator: GenericOperation | StringOperation | NumberOperation;
right: any; right: any;
}; }
export type LogicGroupDefinition = { export interface LogicGroupProps {
logicalOperator: LogicalOperation; logicalOperator: LogicalOperation;
statements: (ExpressionDefinition | LogicGroupDefinition | boolean)[]; statements: ConditionProps[];
}; }
export type ConditionDefinition = ExpressionDefinition | LogicGroupDefinition | boolean; export interface DataConditionProps {
export type ConditionalVariableDefinition = { type: typeof DataConditionType;
type: typeof ConditionalVariableType; condition: ConditionProps;
condition: ConditionDefinition;
ifTrue: any; ifTrue: any;
ifFalse: any; ifFalse: any;
}; }
type DataConditionType = { interface DataConditionPropsDefined extends Omit<DataConditionProps, 'condition'> {
type: typeof ConditionalVariableType;
condition: Condition; condition: Condition;
ifTrue: any; }
ifFalse: any;
}; export class DataCondition extends Model<DataConditionPropsDefined> {
export class DataCondition extends Model<DataConditionType> {
lastEvaluationResult: boolean; lastEvaluationResult: boolean;
private condition: Condition;
private em: EditorModel; private em: EditorModel;
private variableListeners: DynamicVariableListenerManager[] = []; private resolverListeners: DataResolverListener[] = [];
private _onValueChange?: () => void; private _onValueChange?: () => void;
constructor( constructor(
condition: ExpressionDefinition | LogicGroupDefinition | boolean, condition: ConditionProps,
public ifTrue: any, public ifTrue: any,
public ifFalse: any, public ifFalse: any,
opts: { em: EditorModel; onValueChange?: () => void }, opts: { em: EditorModel; onValueChange?: () => void },
@ -54,18 +51,21 @@ export class DataCondition extends Model<DataConditionType> {
const conditionInstance = new Condition(condition, { em: opts.em }); const conditionInstance = new Condition(condition, { em: opts.em });
super({ super({
type: ConditionalVariableType, type: DataConditionType,
condition: conditionInstance, condition: conditionInstance,
ifTrue, ifTrue,
ifFalse, ifFalse,
}); });
this.condition = conditionInstance;
this.em = opts.em; this.em = opts.em;
this.lastEvaluationResult = this.evaluate(); this.lastEvaluationResult = this.evaluate();
this.listenToDataVariables(); this.listenToDataVariables();
this._onValueChange = opts.onValueChange; this._onValueChange = opts.onValueChange;
} }
get condition() {
return this.get('condition')!;
}
evaluate() { evaluate() {
return this.condition.evaluate(); return this.condition.evaluate();
} }
@ -84,7 +84,8 @@ export class DataCondition extends Model<DataConditionType> {
} }
private listenToDataVariables() { private listenToDataVariables() {
if (!this.em) return; const { em } = this;
if (!em) return;
// Clear previous listeners to avoid memory leaks // Clear previous listeners to avoid memory leaks
this.cleanupListeners(); this.cleanupListeners();
@ -92,22 +93,21 @@ export class DataCondition extends Model<DataConditionType> {
const dataVariables = this.getDependentDataVariables(); const dataVariables = this.getDependentDataVariables();
dataVariables.forEach((variable) => { dataVariables.forEach((variable) => {
const variableInstance = new DataVariable(variable, { em: this.em }); const listener = new DataResolverListener({
const listener = new DynamicVariableListenerManager({ em,
em: this.em!, resolver: new DataVariable(variable, { em: this.em }),
dataVariable: variableInstance, onUpdate: (() => {
updateValueFromDataVariable: (() => {
this.reevaluate(); this.reevaluate();
this._onValueChange?.(); this._onValueChange?.();
}).bind(this), }).bind(this),
}); });
this.variableListeners.push(listener); this.resolverListeners.push(listener);
}); });
} }
getDependentDataVariables() { getDependentDataVariables() {
const dataVariables: DataVariableDefinition[] = this.condition.getDataVariables(); const dataVariables: DataVariableProps[] = this.condition.getDataVariables();
if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue); if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue);
if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse); if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse);
@ -115,13 +115,13 @@ export class DataCondition extends Model<DataConditionType> {
} }
private cleanupListeners() { private cleanupListeners() {
this.variableListeners.forEach((listener) => listener.destroy()); this.resolverListeners.forEach((listener) => listener.destroy());
this.variableListeners = []; this.resolverListeners = [];
} }
toJSON() { toJSON() {
return { return {
type: ConditionalVariableType, type: DataConditionType,
condition: this.condition, condition: this.condition,
ifTrue: this.ifTrue, ifTrue: this.ifTrue,
ifFalse: this.ifFalse, ifFalse: this.ifFalse,

7
packages/core/src/data_sources/model/conditional_variables/LogicalGroupStatement.ts

@ -1,14 +1,13 @@
import { LogicalOperator } from './operators/LogicalOperator';
import { ExpressionDefinition, LogicGroupDefinition } from './DataCondition';
import { Condition } from './Condition';
import EditorModel from '../../../editor/model/Editor'; import EditorModel from '../../../editor/model/Editor';
import { Condition, ConditionProps } from './Condition';
import { LogicalOperator } from './operators/LogicalOperator';
export class LogicalGroupStatement { export class LogicalGroupStatement {
private em: EditorModel; private em: EditorModel;
constructor( constructor(
private operator: LogicalOperator, private operator: LogicalOperator,
private statements: (ExpressionDefinition | LogicGroupDefinition | boolean)[], private statements: ConditionProps[],
opts: { em: EditorModel }, opts: { em: EditorModel },
) { ) {
this.em = opts.em; this.em = opts.em;

2
packages/core/src/data_sources/model/conditional_variables/operators/GenericOperator.ts

@ -46,7 +46,7 @@ export class GenericOperator extends Operator {
case 'isBoolean': case 'isBoolean':
return typeof left === 'boolean'; return typeof left === 'boolean';
case 'isDefaultValue': case 'isDefaultValue':
return left instanceof DataVariable && left.get('default') === right; return left instanceof DataVariable && left.get('defaultValue') === right;
default: default:
throw new Error(`Unsupported generic operator: ${this.operator}`); throw new Error(`Unsupported generic operator: ${this.operator}`);
} }

284
packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts

@ -0,0 +1,284 @@
import { isArray } from 'underscore';
import { ObjectAny } from '../../../common';
import Component from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions } from '../../../dom_components/model/types';
import EditorModel from '../../../editor/model/Editor';
import { isObject, serialize, toLowerCase } from '../../../utils/mixins';
import DataResolverListener from '../DataResolverListener';
import DataSource from '../DataSource';
import DataVariable, { DataVariableProps, DataVariableType } from '../DataVariable';
import { isDataVariable } from '../utils';
import { DataCollectionType, keyCollectionDefinition, keyCollectionsStateMap, keyIsCollectionItem } from './constants';
import {
ComponentDataCollectionProps,
DataCollectionConfig,
DataCollectionDataSource,
DataCollectionProps,
DataCollectionState,
DataCollectionStateMap,
} from './types';
import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers';
export default class ComponentDataCollection extends Component {
constructor(props: ComponentDataCollectionProps, opt: ComponentOptions) {
const collectionDef = props[keyCollectionDefinition];
// If we are cloning, leave setting the collection items to the main symbol collection
if (opt.forCloning) {
return super(props as any, opt) as unknown as ComponentDataCollection;
}
const em = opt.em;
const newProps = { ...props, components: undefined, droppable: false } as any;
const cmp: ComponentDataCollection = super(newProps, opt) as unknown as ComponentDataCollection;
if (!collectionDef) {
em.logError('missing collection definition');
return cmp;
}
const parentCollectionStateMap = (props[keyCollectionsStateMap] || {}) as DataCollectionStateMap;
const components: Component[] = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt);
cmp.components(components, opt);
if (isDataVariable(this.collectionDataSource)) {
this.watchDataSource(parentCollectionStateMap, opt);
}
return cmp;
}
get collectionConfig() {
return this.get(keyCollectionDefinition).collectionConfig as DataCollectionConfig;
}
get collectionDataSource() {
return this.collectionConfig.dataSource;
}
toJSON(opts?: ObjectAny) {
const json = super.toJSON.call(this, opts) as ComponentDataCollectionProps;
json[keyCollectionDefinition].componentDef = this.getComponentDef();
delete json.components;
delete json.droppable;
return json;
}
private getComponentDef() {
const firstChild = this.components().at(0);
const firstChildJSON = firstChild ? serialize(firstChild) : this.get(keyCollectionDefinition).componentDef;
delete firstChildJSON?.draggable;
return firstChildJSON;
}
private watchDataSource(parentCollectionStateMap: DataCollectionStateMap, opt: ComponentOptions) {
const { em } = this;
const path = this.collectionDataSource?.path;
if (!path) return;
new DataResolverListener({
em,
resolver: new DataVariable({ type: DataVariableType, path }, { em }),
onUpdate: () => {
const collectionDef = { ...this.get(keyCollectionDefinition), componentDef: this.getComponentDef() };
const collectionItems = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt);
this.components().reset(collectionItems, updateFromWatcher as any);
},
});
}
static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataCollectionType;
}
}
function getCollectionItems(
em: EditorModel,
collectionDef: DataCollectionProps,
parentCollectionStateMap: DataCollectionStateMap,
opt: ComponentOptions,
) {
const { componentDef, collectionConfig } = collectionDef;
const result = validateCollectionConfig(collectionConfig, componentDef, em);
if (!result) {
return [];
}
const components: Component[] = [];
const collectionId = collectionConfig.collectionId;
const items = getDataSourceItems(collectionConfig.dataSource, em);
const startIndex = Math.max(0, collectionConfig.startIndex || 0);
const endIndex = Math.min(
items.length - 1,
collectionConfig.endIndex !== undefined ? collectionConfig.endIndex : Number.MAX_VALUE,
);
const totalItems = endIndex - startIndex + 1;
let symbolMain: Component;
for (let index = startIndex; index <= endIndex; index++) {
const item = items[index];
const collectionState: DataCollectionState = {
collectionId,
currentIndex: index,
currentItem: item,
startIndex: startIndex,
endIndex: endIndex,
totalItems: totalItems,
remainingItems: totalItems - (index + 1),
};
if (parentCollectionStateMap[collectionId]) {
em.logError(
`The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`,
);
return [];
}
const collectionsStateMap: DataCollectionStateMap = {
...parentCollectionStateMap,
[collectionId]: collectionState,
};
if (index === startIndex) {
const componentType = (componentDef?.type as string) || 'default';
let type = em.Components.getType(componentType) || em.Components.getType('default');
const Model = type.model;
symbolMain = new Model({ ...serialize(componentDef), draggable: false }, opt);
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(symbolMain);
}
const instance = symbolMain!.clone({ symbol: true });
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance);
components.push(instance);
}
return components;
}
function setCollectionStateMapAndPropagate(
collectionsStateMap: DataCollectionStateMap,
collectionId: string | undefined,
) {
return (cmp: Component) => {
setCollectionStateMap(collectionsStateMap)(cmp);
const addListener = (component: Component) => {
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(component);
};
const listenerKey = `_hasAddListener${collectionId ? `_${collectionId}` : ''}`;
const cmps = cmp.components();
// Add the 'add' listener if not already in the listeners array
if (!cmp.collectionStateListeners.includes(listenerKey)) {
cmp.listenTo(cmps, 'add', addListener);
cmp.collectionStateListeners.push(listenerKey);
const removeListener = (component: Component) => {
component.stopListening(component.components(), 'add', addListener);
component.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange);
const index = component.collectionStateListeners.indexOf(listenerKey);
if (index > -1) {
component.collectionStateListeners.splice(index, 1);
}
};
cmp.listenTo(cmps, 'remove', removeListener);
}
cmps?.toArray().forEach((component: Component) => {
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(component);
});
cmp.on(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange);
};
}
function handleCollectionStateMapChange(this: Component) {
const updatedCollectionsStateMap = this.get(keyCollectionsStateMap);
this.components()
?.toArray()
.forEach((component: Component) => {
setCollectionStateMap(updatedCollectionsStateMap)(component);
});
}
function logErrorIfMissing(property: any, propertyPath: string, em: EditorModel) {
if (!property) {
em.logError(`The "${propertyPath}" property is required in the collection definition.`);
return false;
}
return true;
}
function validateCollectionConfig(
collectionConfig: DataCollectionConfig,
componentDef: ComponentDefinition,
em: EditorModel,
) {
const validations = [
{ property: collectionConfig, propertyPath: 'collectionConfig' },
{ property: componentDef, propertyPath: 'componentDef' },
{ property: collectionConfig?.collectionId, propertyPath: 'collectionConfig.collectionId' },
{ property: collectionConfig?.dataSource, propertyPath: 'collectionConfig.dataSource' },
];
for (const { property, propertyPath } of validations) {
if (!logErrorIfMissing(property, propertyPath, em)) {
return [];
}
}
return true;
}
function setCollectionStateMap(collectionsStateMap: DataCollectionStateMap) {
return (cmp: Component) => {
cmp.set(keyIsCollectionItem, true);
const updatedCollectionStateMap = {
...cmp.get(keyCollectionsStateMap),
...collectionsStateMap,
};
cmp.set(keyCollectionsStateMap, updatedCollectionStateMap);
cmp.dataResolverWatchers.updateCollectionStateMap(updatedCollectionStateMap);
};
}
function getDataSourceItems(dataSource: DataCollectionDataSource, em: EditorModel) {
let items: DataVariableProps[] = [];
switch (true) {
case isArray(dataSource):
items = dataSource;
break;
case isObject(dataSource) && dataSource instanceof DataSource: {
const id = dataSource.get('id')!;
items = listDataSourceVariables(id, em);
break;
}
case isDataVariable(dataSource): {
const isDataSourceId = dataSource.path.split('.').length === 1;
if (isDataSourceId) {
const id = dataSource.path;
items = listDataSourceVariables(id, em);
} else {
// Path points to a record in the data source
items = em.DataSources.getValue(dataSource.path, []);
}
break;
}
default:
}
return items;
}
function listDataSourceVariables(dataSource_id: string, em: EditorModel): DataVariableProps[] {
const records = em.DataSources.getValue(dataSource_id, []);
const keys = Object.keys(records);
return keys.map((key) => ({
type: DataVariableType,
path: dataSource_id + '.' + key,
}));
}

53
packages/core/src/data_sources/model/data_collection/ComponentDataCollectionVariable.ts

@ -0,0 +1,53 @@
import Component from '../../../dom_components/model/Component';
import { ComponentOptions } from '../../../dom_components/model/types';
import { toLowerCase } from '../../../utils/mixins';
import DataCollectionVariable from './DataCollectionVariable';
import { DataCollectionVariableType, keyCollectionsStateMap } from './constants';
import { ComponentDataCollectionVariableProps, DataCollectionStateMap } from './types';
export default class ComponentDataCollectionVariable extends Component {
dataResolver: DataCollectionVariable;
get defaults() {
// @ts-expect-error
const componentDefaults = super.defaults;
return {
...componentDefaults,
type: DataCollectionVariableType,
collectionId: undefined,
variableType: undefined,
path: undefined,
};
}
constructor(props: ComponentDataCollectionVariableProps, opt: ComponentOptions) {
super(props, opt);
const { type, variableType, path, collectionId } = props;
this.dataResolver = new DataCollectionVariable(
{ type, variableType, path, collectionId },
{
...opt,
collectionsStateMap: this.get(keyCollectionsStateMap),
},
);
this.listenTo(this, `change:${keyCollectionsStateMap}`, this.handleCollectionsMapStateUpdate);
}
private handleCollectionsMapStateUpdate(m: any, v: DataCollectionStateMap, opts = {}) {
this.dataResolver.updateCollectionsStateMap(v);
}
getDataValue() {
return this.dataResolver.getDataValue();
}
getInnerHTML() {
return this.getDataValue();
}
static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataCollectionVariableType;
}
}

158
packages/core/src/data_sources/model/data_collection/DataCollectionVariable.ts

@ -0,0 +1,158 @@
import { DataCollectionVariableProps } from './types';
import { Model } from '../../../common';
import EditorModel from '../../../editor/model/Editor';
import DataVariable, { DataVariableType } from '../DataVariable';
import { DataCollectionVariableType } from './constants';
import { DataCollectionState, DataCollectionStateMap } from './types';
import DataResolverListener from '../DataResolverListener';
interface DataCollectionVariablePropsDefined extends DataCollectionVariableProps {
value?: any;
}
export default class DataCollectionVariable extends Model<DataCollectionVariablePropsDefined> {
em: EditorModel;
collectionsStateMap?: DataCollectionStateMap;
dataVariable?: DataVariable;
resolverListener?: DataResolverListener;
defaults(): Partial<DataCollectionVariablePropsDefined> {
return {
type: DataCollectionVariableType,
collectionId: undefined,
variableType: undefined,
path: undefined,
value: undefined,
};
}
constructor(
props: DataCollectionVariablePropsDefined,
options: {
em: EditorModel;
collectionsStateMap?: DataCollectionStateMap;
},
) {
super(props, options);
this.em = options.em;
this.collectionsStateMap = options.collectionsStateMap;
this.updateDataVariable();
}
hasDynamicValue() {
return !!this.dataVariable;
}
getDataValue() {
const { resolvedValue } = this.updateDataVariable();
if (resolvedValue?.type === DataVariableType) {
return this.dataVariable!.getDataValue();
}
return resolvedValue;
}
private updateDataVariable() {
if (!this.collectionsStateMap) return { resolvedValue: undefined };
const resolvedValue = resolveCollectionVariable(
this.attributes as DataCollectionVariableProps,
this.collectionsStateMap,
this.em,
);
let dataVariable;
if (resolvedValue?.type === DataVariableType) {
dataVariable = new DataVariable(resolvedValue, { em: this.em });
this.dataVariable = dataVariable;
this.resolverListener?.destroy();
this.resolverListener = new DataResolverListener({
em: this.em,
resolver: dataVariable,
onUpdate: () => {
this.set('value', this.dataVariable?.getDataValue());
},
});
}
this.set('value', resolvedValue);
return { resolvedValue, dataVariable };
}
updateCollectionsStateMap(collectionsStateMap: DataCollectionStateMap) {
this.collectionsStateMap = collectionsStateMap;
this.updateDataVariable();
}
destroy() {
this.resolverListener?.destroy();
this.dataVariable?.destroy();
return super.destroy();
}
toJSON(options?: any) {
const json = super.toJSON(options);
delete json.value;
!json.collectionId && delete json.collectionId;
return json;
}
}
function resolveCollectionVariable(
collectionVariableDefinition: DataCollectionVariableProps,
collectionsStateMap: DataCollectionStateMap,
em: EditorModel,
) {
const { collectionId, variableType, path } = collectionVariableDefinition;
if (!collectionsStateMap) return;
const collectionItem = collectionsStateMap[collectionId];
if (!collectionItem) {
return '';
}
if (!variableType) {
em.logError(`Missing collection variable type for collection: ${collectionId}`);
return '';
}
if (variableType === 'currentItem') {
return resolveCurrentItem(collectionItem, path, collectionId, em);
}
return collectionItem[variableType];
}
function resolveCurrentItem(
collectionItem: DataCollectionState,
path: string | undefined,
collectionId: string,
em: EditorModel,
) {
const currentItem = collectionItem.currentItem;
if (!currentItem) {
em.logError(`Current item is missing for collection: ${collectionId}`);
return '';
}
if (currentItem.type === DataVariableType) {
const resolvedPath = currentItem.path ? `${currentItem.path}.${path}` : path;
return {
...currentItem,
path: resolvedPath,
};
}
if (path && !(currentItem as any)[path]) {
em.logError(`Path not found in current item: ${path} for collection: ${collectionId}`);
return '';
}
return path ? (currentItem as any)[path] : currentItem;
}

5
packages/core/src/data_sources/model/data_collection/constants.ts

@ -0,0 +1,5 @@
export const DataCollectionType = 'data-collection';
export const DataCollectionVariableType = 'data-collection-variable';
export const keyCollectionDefinition = 'collectionDef';
export const keyIsCollectionItem = '__is_data_collection_item';
export const keyCollectionsStateMap = '__collections_state_map';

57
packages/core/src/data_sources/model/data_collection/types.ts

@ -0,0 +1,57 @@
import { DataCollectionType, DataCollectionVariableType, keyCollectionDefinition } from './constants';
import { ComponentDefinition, ComponentProperties } from '../../../dom_components/model/types';
import { DataVariableProps } from '../DataVariable';
export type DataCollectionDataSource = DataVariableProps | DataCollectionVariableProps;
export interface DataCollectionConfig {
collectionId: string;
startIndex?: number;
endIndex?: number;
dataSource: DataCollectionDataSource;
}
export enum DataCollectionStateVariableType {
currentIndex = 'currentIndex',
startIndex = 'startIndex',
currentItem = 'currentItem',
endIndex = 'endIndex',
collectionId = 'collectionId',
totalItems = 'totalItems',
remainingItems = 'remainingItems',
}
export interface DataCollectionState {
[DataCollectionStateVariableType.currentIndex]: number;
[DataCollectionStateVariableType.startIndex]: number;
[DataCollectionStateVariableType.currentItem]: DataVariableProps;
[DataCollectionStateVariableType.endIndex]: number;
[DataCollectionStateVariableType.collectionId]: string;
[DataCollectionStateVariableType.totalItems]: number;
[DataCollectionStateVariableType.remainingItems]: number;
}
export interface DataCollectionStateMap {
[key: string]: DataCollectionState;
}
export interface ComponentDataCollectionProps extends ComponentDefinition {
[keyCollectionDefinition]: DataCollectionProps;
}
export interface ComponentDataCollectionVariableProps
extends DataCollectionVariableProps,
Omit<ComponentProperties, 'type'> {}
export interface DataCollectionProps {
type: typeof DataCollectionType;
collectionConfig: DataCollectionConfig;
componentDef: ComponentDefinition;
}
export interface DataCollectionVariableProps {
type: typeof DataCollectionVariableType;
variableType: DataCollectionStateVariableType;
collectionId: string;
path?: string;
}

60
packages/core/src/data_sources/model/utils.ts

@ -1,50 +1,68 @@
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import { DynamicValue, DynamicValueDefinition } from '../types'; import { DataResolver, DataResolverProps } from '../types';
import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition'; import { DataCollectionStateMap } from './data_collection/types';
import DataVariable, { DataVariableType } from './DataVariable'; import DataCollectionVariable from './data_collection/DataCollectionVariable';
import { DataCollectionVariableType } from './data_collection/constants';
import { DataConditionType, DataCondition } from './conditional_variables/DataCondition';
import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable';
export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition { export function isDataResolverProps(value: any): value is DataResolverProps {
return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value?.type); return (
typeof value === 'object' && [DataVariableType, DataConditionType, DataCollectionVariableType].includes(value?.type)
);
} }
export function isDynamicValue(value: any): value is DynamicValue { export function isDataResolver(value: any): value is DataResolver {
return value instanceof DataVariable || value instanceof DataCondition; return value instanceof DataVariable || value instanceof DataCondition;
} }
export function isDataVariable(variable: any) { export function isDataVariable(variable: any): variable is DataVariableProps {
return variable?.type === DataVariableType; return variable?.type === DataVariableType;
} }
export function isDataCondition(variable: any) { export function isDataCondition(variable: any) {
return variable?.type === ConditionalVariableType; return variable?.type === DataConditionType;
} }
export function evaluateVariable(variable: any, em: EditorModel) { export function evaluateVariable(variable: any, em: EditorModel) {
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable; return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable;
} }
export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, em: EditorModel): DynamicValue { export function getDataResolverInstance(
const dynamicType = valueDefinition.type; resolverProps: DataResolverProps,
let dynamicVariable: DynamicValue; options: { em: EditorModel; collectionsStateMap?: DataCollectionStateMap },
): DataResolver {
const { type } = resolverProps;
let resolver: DataResolver;
switch (dynamicType) { switch (type) {
case DataVariableType: case DataVariableType:
dynamicVariable = new DataVariable(valueDefinition, { em: em }); resolver = new DataVariable(resolverProps, options);
break; break;
case ConditionalVariableType: { case DataConditionType: {
const { condition, ifTrue, ifFalse } = valueDefinition; const { condition, ifTrue, ifFalse } = resolverProps;
dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: em }); resolver = new DataCondition(condition, ifTrue, ifFalse, options);
break;
}
case DataCollectionVariableType: {
resolver = new DataCollectionVariable(resolverProps, options);
break; break;
} }
default: default:
throw new Error(`Unsupported dynamic type: ${dynamicType}`); throw new Error(`Unsupported dynamic type: ${type}`);
} }
return dynamicVariable; return resolver;
} }
export function evaluateDynamicValueDefinition(valueDefinition: DynamicValueDefinition, em: EditorModel) { export function getDataResolverInstanceValue(
const dynamicVariable = getDynamicValueInstance(valueDefinition, em); resolverProps: DataResolverProps,
options: {
em: EditorModel;
collectionsStateMap?: DataCollectionStateMap;
},
) {
const resolver = getDataResolverInstance(resolverProps, options);
return { variable: dynamicVariable, value: dynamicVariable.getDataValue() }; return { resolver, value: resolver.getDataValue() };
} }

19
packages/core/src/data_sources/types.ts

@ -1,12 +1,15 @@
import { ObjectAny } from '../common'; import { Model, Collection, ObjectAny } from '../common';
import ComponentDataVariable from './model/ComponentDataVariable'; import DataCollectionVariable from './model/data_collection/DataCollectionVariable';
import { DataCollectionVariableProps } from './model/data_collection/types';
import DataRecord from './model/DataRecord'; import DataRecord from './model/DataRecord';
import DataRecords from './model/DataRecords'; import DataRecords from './model/DataRecords';
import DataVariable, { DataVariableDefinition } from './model/DataVariable'; import DataVariable, { DataVariableProps } from './model/DataVariable';
import { ConditionalVariableDefinition, DataCondition } from './model/conditional_variables/DataCondition'; import { DataConditionProps, DataCondition } from './model/conditional_variables/DataCondition';
export type DataResolver = DataVariable | DataCondition | DataCollectionVariable;
export type DataResolverProps = DataVariableProps | DataConditionProps | DataCollectionVariableProps;
export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition;
export type DynamicValueDefinition = DataVariableDefinition | ConditionalVariableDefinition;
export interface DataRecordProps extends ObjectAny { export interface DataRecordProps extends ObjectAny {
/** /**
* Record id. * Record id.
@ -21,8 +24,8 @@ export interface DataRecordProps extends ObjectAny {
[key: string]: any; [key: string]: any;
} }
export interface DataVariableListener { export interface DataSourceListener {
obj: any; obj: Model | Collection;
event: string; event: string;
} }

21
packages/core/src/data_sources/view/ComponentDataCollectionVariableView.ts

@ -0,0 +1,21 @@
import ComponentView from '../../dom_components/view/ComponentView';
import DataResolverListener from '../model/DataResolverListener';
import ComponentDataCollectionVariable from '../model/data_collection/ComponentDataCollectionVariable';
export default class ComponentDataCollectionVariableView extends ComponentView<ComponentDataCollectionVariable> {
dataResolverListener?: DataResolverListener;
initialize(opt = {}) {
super.initialize(opt);
this.dataResolverListener = new DataResolverListener({
em: this.em!,
resolver: this.model.dataResolver,
onUpdate: this.postRender.bind(this),
});
}
postRender() {
this.el.innerHTML = this.model.getDataValue();
super.postRender();
}
}

4
packages/core/src/data_sources/view/ComponentDataCollectionView.ts

@ -0,0 +1,4 @@
import ComponentView from '../../dom_components/view/ComponentView';
import ComponentDataCollection from '../model/data_collection/ComponentDataCollection';
export default class ComponentDataCollectionView extends ComponentView<ComponentDataCollection> {}

4
packages/core/src/data_sources/view/ComponentDataConditionView.ts

@ -0,0 +1,4 @@
import ComponentView from '../../dom_components/view/ComponentView';
import ComponentDataCondition from '../model/conditional_variables/ComponentDataCondition';
export default class ComponentDataConditionView extends ComponentView<ComponentDataCondition> {}

21
packages/core/src/data_sources/view/ComponentDataVariableView.ts

@ -1,23 +1,26 @@
import ComponentView from '../../dom_components/view/ComponentView'; import ComponentView from '../../dom_components/view/ComponentView';
import ComponentDataVariable from '../model/ComponentDataVariable'; import ComponentDataVariable from '../model/ComponentDataVariable';
import DynamicVariableListenerManager from '../model/DataVariableListenerManager'; import DataResolverListener from '../model/DataResolverListener';
export default class ComponentDataVariableView extends ComponentView<ComponentDataVariable> { export default class ComponentDataVariableView extends ComponentView<ComponentDataVariable> {
dynamicVariableListener?: DynamicVariableListenerManager; dataResolverListener!: DataResolverListener;
initialize(opt = {}) { initialize(opt = {}) {
super.initialize(opt); super.initialize(opt);
this.dynamicVariableListener = new DynamicVariableListenerManager({ this.dataResolverListener = new DataResolverListener({
em: this.em!, em: this.em,
dataVariable: this.model, resolver: this.model.dataResolver,
updateValueFromDataVariable: () => this.postRender(), onUpdate: () => this.postRender(),
}); });
} }
remove() {
this.dataResolverListener.destroy();
return super.remove();
}
postRender() { postRender() {
const { model, el, em } = this; this.el.innerHTML = this.model.getDataValue();
const { path, defaultValue } = model.attributes;
el.innerHTML = em.DataSources.getValue(path, defaultValue);
super.postRender(); super.postRender();
} }
} }

4
packages/core/src/data_sources/view/ComponentDynamicView.ts

@ -1,4 +0,0 @@
import ComponentView from '../../dom_components/view/ComponentView';
import ConditionalComponent from '../model/conditional_variables/ConditionalComponent';
export default class ConditionalComponentView extends ComponentView<ConditionalComponent> {}

29
packages/core/src/dom_components/index.ts

@ -53,7 +53,7 @@
* *
* @module Components * @module Components
*/ */
import { debounce, isArray, isBoolean, isEmpty, isFunction, isString, isSymbol, result } from 'underscore'; import { debounce, isArray, isEmpty, isFunction, isString, isSymbol, result } from 'underscore';
import { ItemManagerModule } from '../abstract/Module'; import { ItemManagerModule } from '../abstract/Module';
import { ObjectAny } from '../common'; import { ObjectAny } from '../common';
import EditorModel from '../editor/model/Editor'; import EditorModel from '../editor/model/Editor';
@ -125,9 +125,14 @@ import { BlockProperties } from '../block_manager/model/Block';
import ComponentDataVariable from '../data_sources/model/ComponentDataVariable'; import ComponentDataVariable from '../data_sources/model/ComponentDataVariable';
import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView'; import ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView';
import { DataVariableType } from '../data_sources/model/DataVariable'; import { DataVariableType } from '../data_sources/model/DataVariable';
import { ConditionalVariableType } from '../data_sources/model/conditional_variables/DataCondition'; import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition';
import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ConditionalComponent'; import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition';
import ConditionalComponentView from '../data_sources/view/ComponentDynamicView'; import ComponentDataConditionView from '../data_sources/view/ComponentDataConditionView';
import ComponentDataCollection from '../data_sources/model/data_collection/ComponentDataCollection';
import { DataCollectionType, DataCollectionVariableType } from '../data_sources/model/data_collection/constants';
import ComponentDataCollectionVariable from '../data_sources/model/data_collection/ComponentDataCollectionVariable';
import ComponentDataCollectionVariableView from '../data_sources/view/ComponentDataCollectionVariableView';
import ComponentDataCollectionView from '../data_sources/view/ComponentDataCollectionView';
export type ComponentEvent = export type ComponentEvent =
| 'component:create' | 'component:create'
@ -194,9 +199,19 @@ export interface CanMoveResult {
export default class ComponentManager extends ItemManagerModule<DomComponentsConfig, any> { export default class ComponentManager extends ItemManagerModule<DomComponentsConfig, any> {
componentTypes: ComponentStackItem[] = [ componentTypes: ComponentStackItem[] = [
{ {
id: ConditionalVariableType, id: DataCollectionVariableType,
model: ComponentConditionalVariable, model: ComponentDataCollectionVariable,
view: ConditionalComponentView, view: ComponentDataCollectionVariableView,
},
{
id: DataCollectionType,
model: ComponentDataCollection,
view: ComponentDataCollectionView,
},
{
id: DataConditionType,
model: ComponentDataCondition,
view: ComponentDataConditionView,
}, },
{ {
id: DataVariableType, id: DataVariableType,

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

@ -51,15 +51,11 @@ import {
updateSymbolComps, updateSymbolComps,
updateSymbolProps, updateSymbolProps,
} from './SymbolUtils'; } from './SymbolUtils';
import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher'; import { ComponentDataResolverWatchers } from './ComponentDataResolverWatchers';
import { DynamicValueWatcher } from './DynamicValueWatcher'; import { DynamicWatchersOptions } from './ComponentResolverWatcher';
import { DynamicValueDefinition } from '../../data_sources/types'; import { keyIsCollectionItem, keyCollectionsStateMap } from '../../data_sources/model/data_collection/constants';
export interface IComponent extends ExtractMethods<Component> {} export interface IComponent extends ExtractMethods<Component> {}
export interface DynamicWatchersOptions {
skipWatcherUpdates?: boolean;
fromDataSource?: boolean;
}
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {} export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {}
export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions {} export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions {}
@ -262,12 +258,20 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @private * @private
* @ts-ignore */ * @ts-ignore */
collection!: Components; collection!: Components;
componentDVListener: ComponentDynamicValueWatcher; collectionStateListeners: string[] = [];
dataResolverWatchers: ComponentDataResolverWatchers;
constructor(props: ComponentProperties = {}, opt: ComponentOptions) { constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt); const dataResolverWatchers = new ComponentDataResolverWatchers(undefined, {
this.componentDVListener = new ComponentDynamicValueWatcher(this, opt.em); em: opt.em,
this.componentDVListener.addProps(props); collectionsStateMap: props[keyCollectionsStateMap],
});
super(props, {
...opt,
dataResolverWatchers,
} as any);
dataResolverWatchers.bindComponent(this);
this.dataResolverWatchers = dataResolverWatchers;
bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps'); bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps');
const em = opt.em; const em = opt.em;
@ -295,9 +299,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.opt = opt; this.opt = opt;
this.em = em!; this.em = em!;
this.config = opt.config || {}; this.config = opt.config || {};
const dynamicAttributes = this.dataResolverWatchers.getDynamicAttributesDefs();
this.setAttributes({ this.setAttributes({
...(result(this, 'defaults').attributes || {}), ...(result(this, 'defaults').attributes || {}),
...(this.get('attributes') || {}), ...(this.get('attributes') || {}),
...dynamicAttributes,
}); });
this.ccid = Component.createId(this, opt); this.ccid = Component.createId(this, opt);
this.preInit(); this.preInit();
@ -343,7 +349,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
optionsOrUndefined?: ComponentSetOptions, optionsOrUndefined?: ComponentSetOptions,
): this { ): this {
let attributes: Partial<ComponentProperties>; let attributes: Partial<ComponentProperties>;
let options: ComponentSetOptions = { skipWatcherUpdates: false, fromDataSource: false }; let options: ComponentSetOptions & {
dataResolverWatchers?: ComponentDataResolverWatchers;
} = { skipWatcherUpdates: false, fromDataSource: false };
if (typeof keyOrAttributes === 'object') { if (typeof keyOrAttributes === 'object') {
attributes = keyOrAttributes; attributes = keyOrAttributes;
options = valueOrOptions || (options as ComponentSetOptions); options = valueOrOptions || (options as ComponentSetOptions);
@ -355,16 +363,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
options = optionsOrUndefined || options; options = optionsOrUndefined || options;
} }
// @ts-ignore this.dataResolverWatchers = this.dataResolverWatchers || options.dataResolverWatchers;
const em = this.em || options.em; const evaluatedProps = this.dataResolverWatchers.addProps(attributes, options);
const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attributes, em);
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource;
if (!shouldSkipWatcherUpdates) {
this.componentDVListener?.addProps(attributes);
}
return super.set(evaluatedAttributes, options); return super.set(evaluatedProps, options);
} }
__postAdd(opts: { recursive?: boolean } = {}) { __postAdd(opts: { recursive?: boolean } = {}) {
@ -503,8 +505,13 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @example * @example
* component.setSymbolOverride(['children', 'classes']); * component.setSymbolOverride(['children', 'classes']);
*/ */
setSymbolOverride(value?: boolean | string | string[]) { setSymbolOverride(value: boolean | string | string[], options: DynamicWatchersOptions = {}) {
this.set(keySymbolOvrd, (isString(value) ? [value] : value) ?? 0); this.set(
{
[keySymbolOvrd]: (isString(value) ? [value] : value) ?? 0,
},
options,
);
} }
/** /**
@ -685,14 +692,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* component.setAttributes({ id: 'test', 'data-key': 'value' }); * component.setAttributes({ id: 'test', 'data-key': 'value' });
*/ */
setAttributes(attrs: ObjectAny, opts: SetAttrOptions = { skipWatcherUpdates: false, fromDataSource: false }) { setAttributes(attrs: ObjectAny, opts: SetAttrOptions = { skipWatcherUpdates: false, fromDataSource: false }) {
// @ts-ignore this.set('attributes', { ...attrs }, opts);
const em = this.em || opts.em;
const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attrs, em);
const shouldSkipWatcherUpdates = opts.skipWatcherUpdates || opts.fromDataSource;
if (!shouldSkipWatcherUpdates) {
this.componentDVListener.setAttributes(attrs);
}
this.set('attributes', { ...evaluatedAttributes }, opts);
return this; return this;
} }
@ -706,7 +706,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* component.addAttributes({ 'data-key': 'value' }); * component.addAttributes({ 'data-key': 'value' });
*/ */
addAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) { addAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) {
const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs(); const dynamicAttributes = this.dataResolverWatchers.getDynamicAttributesDefs();
return this.setAttributes( return this.setAttributes(
{ {
...this.getAttributes({ noClass: true }), ...this.getAttributes({ noClass: true }),
@ -728,7 +728,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/ */
removeAttributes(attrs: string | string[] = [], opts: SetOptions = {}) { removeAttributes(attrs: string | string[] = [], opts: SetOptions = {}) {
const attrArr = Array.isArray(attrs) ? attrs : [attrs]; const attrArr = Array.isArray(attrs) ? attrs : [attrs];
this.componentDVListener.removeAttributes(attrArr); this.dataResolverWatchers.removeAttributes(attrArr);
const compAttr = this.getAttributes(); const compAttr = this.getAttributes();
attrArr.map((i) => delete compAttr[i]); attrArr.map((i) => delete compAttr[i]);
@ -965,12 +965,12 @@ export default class Component extends StyleableModel<ComponentProperties> {
const value = trait.getInitValue(); const value = trait.getInitValue();
if (trait.changeProp) { if (trait.changeProp) {
this.set(name, value); isUndefined(this.get(name)) && this.set(name, value);
} else { } else {
if (name && value) attrs[name] = value; if (name && value) attrs[name] = value;
} }
}); });
const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs(); const dynamicAttributes = this.dataResolverWatchers.getDynamicAttributesDefs();
traits.length && traits.length &&
this.setAttributes({ this.setAttributes({
...attrs, ...attrs,
@ -1318,13 +1318,14 @@ export default class Component extends StyleableModel<ComponentProperties> {
clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this { clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this {
const em = this.em; const em = this.em;
const attr = { const attr = {
...this.componentDVListener.getPropsDefsOrValues(this.attributes), ...this.attributes,
...this.dataResolverWatchers.getDynamicPropsDefs(),
}; };
const opts = { ...this.opt }; const opts = { ...this.opt };
const id = this.getId(); const id = this.getId();
const cssc = em?.Css; const cssc = em?.Css;
attr.attributes = { attr.attributes = {
...(attr.attributes ? this.componentDVListener.getAttributesDefsOrValues(attr.attributes) : undefined), ...(attr.attributes ? this.dataResolverWatchers.getAttributesDefsOrValues(attr.attributes) : undefined),
}; };
// @ts-ignore // @ts-ignore
attr.components = []; attr.components = [];
@ -1353,6 +1354,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
attr.status = ''; attr.status = '';
// @ts-ignore // @ts-ignore
opts.collection = null; opts.collection = null;
opts.forCloning = true;
// @ts-ignore // @ts-ignore
const cloned = new this.constructor(attr, opts); const cloned = new this.constructor(attr, opts);
@ -1581,9 +1583,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/ */
toJSON(opts: ObjectAny = {}): ComponentDefinition { toJSON(opts: ObjectAny = {}): ComponentDefinition {
let obj = Model.prototype.toJSON.call(this, opts); let obj = Model.prototype.toJSON.call(this, opts);
obj = { ...obj, ...this.componentDVListener.getDynamicPropsDefs() }; obj = { ...obj, ...this.dataResolverWatchers.getDynamicPropsDefs() };
obj.attributes = this.componentDVListener.getAttributesDefsOrValues(this.getAttributes()); obj.attributes = this.dataResolverWatchers.getAttributesDefsOrValues(this.getAttributes());
delete obj.componentDVListener; delete obj.dataResolverWatchers;
delete obj.attributes.class; delete obj.attributes.class;
delete obj.toolbar; delete obj.toolbar;
delete obj.traits; delete obj.traits;
@ -1591,6 +1593,14 @@ export default class Component extends StyleableModel<ComponentProperties> {
delete obj.open; // used in Layers delete obj.open; // used in Layers
delete obj._undoexc; delete obj._undoexc;
delete obj.delegate; delete obj.delegate;
if (this.get(keyIsCollectionItem)) {
delete obj[keySymbol];
delete obj[keySymbolOvrd];
delete obj[keySymbols];
delete obj[keyCollectionsStateMap];
delete obj[keyIsCollectionItem];
delete obj.attributes.id;
}
if (!opts.fromUndo) { if (!opts.fromUndo) {
const symbol = obj[keySymbol]; const symbol = obj[keySymbol];
@ -1657,9 +1667,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @return {this} * @return {this}
*/ */
setId(id: string, opts?: SetOptions & { idUpdate?: boolean }) { setId(id: string, opts?: SetOptions & { idUpdate?: boolean }) {
const attrs = { ...this.get('attributes') }; this.addAttributes({ id }, opts);
attrs.id = id;
this.set('attributes', attrs, opts);
return this; return this;
} }
@ -1822,7 +1830,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
} }
destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR {
this.componentDVListener.destroy(); this.dataResolverWatchers.destroy();
return super.destroy(options); return super.destroy(options);
} }

116
packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts

@ -0,0 +1,116 @@
import { ObjectAny } from '../../common';
import {
DataCollectionVariableType,
keyCollectionsStateMap,
keyIsCollectionItem,
} from '../../data_sources/model/data_collection/constants';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import Component from './Component';
import {
ComponentResolverWatcher,
ComponentResolverWatcherOptions,
DynamicWatchersOptions,
} from './ComponentResolverWatcher';
import { getSymbolsToUpdate } from './SymbolUtils';
export const updateFromWatcher = { fromDataSource: true, avoidStore: true };
export class ComponentDataResolverWatchers {
private propertyWatcher: ComponentResolverWatcher;
private attributeWatcher: ComponentResolverWatcher;
constructor(
private component: Component | undefined,
options: ComponentResolverWatcherOptions,
) {
this.propertyWatcher = new ComponentResolverWatcher(component, this.onPropertyUpdate, options);
this.attributeWatcher = new ComponentResolverWatcher(component, this.onAttributeUpdate, options);
}
private onPropertyUpdate(component: Component | undefined, key: string, value: any) {
component?.set(key, value, updateFromWatcher);
}
private onAttributeUpdate(component: Component | undefined, key: string, value: any) {
component?.addAttributes({ [key]: value }, updateFromWatcher);
}
bindComponent(component: Component) {
this.component = component;
this.propertyWatcher.bindComponent(component);
this.attributeWatcher.bindComponent(component);
this.updateSymbolOverride();
}
updateCollectionStateMap(collectionsStateMap: DataCollectionStateMap) {
this.propertyWatcher.updateCollectionStateMap(collectionsStateMap);
this.attributeWatcher.updateCollectionStateMap(collectionsStateMap);
}
addProps(props: ObjectAny, options: DynamicWatchersOptions = {}) {
const excludedFromEvaluation = ['components'];
const evaluatedProps = Object.fromEntries(
Object.entries(props).map(([key, value]) =>
excludedFromEvaluation.includes(key)
? [key, value] // Return excluded keys as they are
: [key, this.propertyWatcher.addDynamicValues({ [key]: value }, options)[key]],
),
);
if (props.attributes) {
const evaluatedAttributes = this.attributeWatcher.setDynamicValues(props.attributes, options);
evaluatedProps['attributes'] = evaluatedAttributes;
}
const skipOverrideUpdates = options.skipWatcherUpdates || options.fromDataSource;
if (!skipOverrideUpdates) {
this.updateSymbolOverride();
}
return evaluatedProps;
}
removeAttributes(attributes: string[]) {
this.attributeWatcher.removeListeners(attributes);
this.updateSymbolOverride();
}
private updateSymbolOverride() {
if (!this.component || !this.component.get(keyIsCollectionItem)) return;
const keys = this.propertyWatcher.getDynamicValuesOfType(DataCollectionVariableType);
const attributesKeys = this.attributeWatcher.getDynamicValuesOfType(DataCollectionVariableType);
const combinedKeys = [keyCollectionsStateMap, ...keys];
const haveOverridenAttributes = Object.keys(attributesKeys).length;
if (haveOverridenAttributes) combinedKeys.push('attributes');
const toUp = getSymbolsToUpdate(this.component);
toUp.forEach((child) => {
child.setSymbolOverride(combinedKeys, { fromDataSource: true });
});
this.component.setSymbolOverride(combinedKeys, { fromDataSource: true });
}
getDynamicPropsDefs() {
return this.propertyWatcher.getAllSerializableValues();
}
getDynamicAttributesDefs() {
return this.attributeWatcher.getAllSerializableValues();
}
getPropsDefsOrValues(props: ObjectAny) {
return this.propertyWatcher.getSerializableValues(props);
}
getAttributesDefsOrValues(attributes: ObjectAny) {
return this.attributeWatcher.getSerializableValues(attributes);
}
destroy() {
this.propertyWatcher.destroy();
this.attributeWatcher.destroy();
}
}

66
packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts

@ -1,66 +0,0 @@
import { ObjectAny } from '../../common';
import EditorModel from '../../editor/model/Editor';
import Component from './Component';
import { DynamicValueWatcher } from './DynamicValueWatcher';
export class ComponentDynamicValueWatcher {
private propertyWatcher: DynamicValueWatcher;
private attributeWatcher: DynamicValueWatcher;
constructor(
private component: Component,
em: EditorModel,
) {
this.propertyWatcher = new DynamicValueWatcher(this.createPropertyUpdater(), em);
this.attributeWatcher = new DynamicValueWatcher(this.createAttributeUpdater(), em);
}
private createPropertyUpdater() {
return (key: string, value: any) => {
this.component.set(key, value, { fromDataSource: true, avoidStore: true });
};
}
private createAttributeUpdater() {
return (key: string, value: any) => {
this.component.addAttributes({ [key]: value }, { fromDataSource: true, avoidStore: true });
};
}
addProps(props: ObjectAny) {
this.propertyWatcher.addDynamicValues(props);
}
addAttributes(attributes: ObjectAny) {
this.attributeWatcher.addDynamicValues(attributes);
}
setAttributes(attributes: ObjectAny) {
this.attributeWatcher.setDynamicValues(attributes);
}
removeAttributes(attributes: string[]) {
this.attributeWatcher.removeListeners(attributes);
}
getDynamicPropsDefs() {
return this.propertyWatcher.getAllSerializableValues();
}
getDynamicAttributesDefs() {
return this.attributeWatcher.getAllSerializableValues();
}
getAttributesDefsOrValues(attributes: ObjectAny) {
return this.attributeWatcher.getSerializableValues(attributes);
}
getPropsDefsOrValues(props: ObjectAny) {
return this.propertyWatcher.getSerializableValues(props);
}
destroy() {
this.propertyWatcher.removeListeners();
this.attributeWatcher.removeListeners();
}
}

180
packages/core/src/dom_components/model/ComponentResolverWatcher.ts

@ -0,0 +1,180 @@
import { ObjectAny } from '../../common';
import { DataCollectionVariableType } from '../../data_sources/model/data_collection/constants';
import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
import DataResolverListener from '../../data_sources/model/DataResolverListener';
import { getDataResolverInstanceValue, isDataResolverProps } from '../../data_sources/model/utils';
import EditorModel from '../../editor/model/Editor';
import { DataResolverProps } from '../../data_sources/types';
import Component from './Component';
export interface DynamicWatchersOptions {
skipWatcherUpdates?: boolean;
fromDataSource?: boolean;
}
export interface ComponentResolverWatcherOptions {
em: EditorModel;
collectionsStateMap?: DataCollectionStateMap;
}
type UpdateFn = (component: Component | undefined, key: string, value: any) => void;
export class ComponentResolverWatcher {
private em: EditorModel;
private collectionsStateMap?: DataCollectionStateMap;
private resolverListeners: Record<string, DataResolverListener> = {};
constructor(
private component: Component | undefined,
private updateFn: UpdateFn,
options: ComponentResolverWatcherOptions,
) {
this.em = options.em;
this.collectionsStateMap = options.collectionsStateMap;
}
bindComponent(component: Component) {
this.component = component;
}
updateCollectionStateMap(collectionsStateMap: DataCollectionStateMap) {
this.collectionsStateMap = collectionsStateMap;
const collectionVariablesKeys = this.getDynamicValuesOfType(DataCollectionVariableType);
const collectionVariablesObject = collectionVariablesKeys.reduce(
(acc: { [key: string]: DataResolverProps | null }, key) => {
acc[key] = null;
return acc;
},
{},
);
const newVariables = this.getSerializableValues(collectionVariablesObject);
const evaluatedValues = this.addDynamicValues(newVariables);
Object.keys(evaluatedValues).forEach((key) => {
this.updateFn(this.component, key, evaluatedValues[key]);
});
}
setDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) {
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource;
if (!shouldSkipWatcherUpdates) {
this.removeListeners();
}
return this.addDynamicValues(values, options);
}
addDynamicValues(values: ObjectAny | undefined, options: DynamicWatchersOptions = {}) {
if (!values) return {};
const evaluatedValues = this.evaluateValues(values);
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource;
if (!shouldSkipWatcherUpdates) {
this.updateListeners(values);
}
return evaluatedValues;
}
private updateListeners(values: { [key: string]: any }) {
const { em, collectionsStateMap } = this;
this.removeListeners(Object.keys(values));
const propsKeys = Object.keys(values);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
const resolverProps = values[key];
if (!isDataResolverProps(resolverProps)) {
continue;
}
const { resolver } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap });
this.resolverListeners[key] = new DataResolverListener({
em,
resolver,
onUpdate: (value) => this.updateFn.bind(this)(this.component, key, value),
});
}
}
private evaluateValues(values: ObjectAny) {
const { em, collectionsStateMap } = this;
const evaluatedValues = { ...values };
const propsKeys = Object.keys(values);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
const resolverProps = values[key];
if (!isDataResolverProps(resolverProps)) {
continue;
}
const { value } = getDataResolverInstanceValue(resolverProps, { em, collectionsStateMap });
evaluatedValues[key] = value;
}
return evaluatedValues;
}
/**
* removes listeners to stop watching for changes,
* if keys argument is omitted, remove all listeners
* @argument keys
*/
removeListeners(keys?: string[]) {
const propsKeys = keys ? keys : Object.keys(this.resolverListeners);
propsKeys.forEach((key) => {
if (this.resolverListeners[key]) {
this.resolverListeners[key].destroy?.();
delete this.resolverListeners[key];
}
});
return propsKeys;
}
getSerializableValues(values: ObjectAny | undefined) {
if (!values) return {};
const serializableValues = { ...values };
const propsKeys = Object.keys(serializableValues);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
const resolverListener = this.resolverListeners[key];
if (resolverListener) {
serializableValues[key] = resolverListener.resolver.toJSON();
}
}
return serializableValues;
}
getAllSerializableValues() {
const serializableValues: ObjectAny = {};
const propsKeys = Object.keys(this.resolverListeners);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
serializableValues[key] = this.resolverListeners[key].resolver.toJSON();
}
return serializableValues;
}
getDynamicValuesOfType(type: DataResolverProps['type']) {
const keys = Object.keys(this.resolverListeners).filter((key: string) => {
// @ts-ignore
return this.resolverListeners[key].resolver.get('type') === type;
});
return keys;
}
destroy() {
this.removeListeners();
}
}

117
packages/core/src/dom_components/model/DynamicValueWatcher.ts

@ -1,117 +0,0 @@
import { ObjectAny } from '../../common';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
import { evaluateDynamicValueDefinition, isDynamicValueDefinition } from '../../data_sources/model/utils';
import { DynamicValue } from '../../data_sources/types';
import EditorModel from '../../editor/model/Editor';
export class DynamicValueWatcher {
dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {};
constructor(
private updateFn: (key: string, value: any) => void,
private em: EditorModel,
) {}
static getStaticValues(values: ObjectAny | undefined, em: EditorModel): ObjectAny {
if (!values) return {};
const evaluatedValues: ObjectAny = { ...values };
const propsKeys = Object.keys(values);
for (const key of propsKeys) {
const valueDefinition = values[key];
if (!isDynamicValueDefinition(valueDefinition)) continue;
const { value } = evaluateDynamicValueDefinition(valueDefinition, em);
evaluatedValues[key] = value;
}
return evaluatedValues;
}
static areStaticValues(values: ObjectAny | undefined) {
if (!values) return true;
return Object.keys(values).every((key) => {
return !isDynamicValueDefinition(values[key]);
});
}
setDynamicValues(values: ObjectAny | undefined) {
this.removeListeners();
return this.addDynamicValues(values);
}
addDynamicValues(values: ObjectAny | undefined) {
if (!values) return {};
this.removeListeners(Object.keys(values));
const dynamicProps = this.getDynamicValues(values);
const propsKeys = Object.keys(dynamicProps);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
this.dynamicVariableListeners[key] = new DynamicVariableListenerManager({
em: this.em,
dataVariable: dynamicProps[key],
updateValueFromDataVariable: (value: any) => {
this.updateFn.bind(this)(key, value);
},
});
}
return dynamicProps;
}
private getDynamicValues(values: ObjectAny) {
const dynamicValues: {
[key: string]: DynamicValue;
} = {};
const propsKeys = Object.keys(values);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
if (!isDynamicValueDefinition(values[key])) {
continue;
}
const { variable } = evaluateDynamicValueDefinition(values[key], this.em);
dynamicValues[key] = variable;
}
return dynamicValues;
}
/**
* removes listeners to stop watching for changes,
* if keys argument is omitted, remove all listeners
* @argument keys
*/
removeListeners(keys?: string[]) {
const propsKeys = keys ? keys : Object.keys(this.dynamicVariableListeners);
propsKeys.forEach((key) => {
if (this.dynamicVariableListeners[key]) {
this.dynamicVariableListeners[key].destroy();
delete this.dynamicVariableListeners[key];
}
});
}
getSerializableValues(values: ObjectAny | undefined) {
if (!values) return {};
const serializableValues = { ...values };
const propsKeys = Object.keys(serializableValues);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
if (this.dynamicVariableListeners[key]) {
serializableValues[key] = this.dynamicVariableListeners[key].dynamicVariable.toJSON();
}
}
return serializableValues;
}
getAllSerializableValues() {
const serializableValues: ObjectAny = {};
const propsKeys = Object.keys(this.dynamicVariableListeners);
for (let index = 0; index < propsKeys.length; index++) {
const key = propsKeys[index];
serializableValues[key] = this.dynamicVariableListeners[key].dynamicVariable.toJSON();
}
return serializableValues;
}
}

78
packages/core/src/dom_components/model/SymbolUtils.ts

@ -3,6 +3,10 @@ import Component, { keySymbol, keySymbolOvrd, keySymbols } from './Component';
import { SymbolToUpOptions } from './types'; import { SymbolToUpOptions } from './types';
import { isEmptyObj } from '../../utils/mixins'; import { isEmptyObj } from '../../utils/mixins';
import Components from './Components'; import Components from './Components';
import {
DataCollectionVariableType,
keyCollectionDefinition,
} from '../../data_sources/model/data_collection/constants';
export const isSymbolMain = (cmp: Component) => isArray(cmp.get(keySymbols)); export const isSymbolMain = (cmp: Component) => isArray(cmp.get(keySymbols));
@ -129,38 +133,58 @@ export const logSymbol = (symb: Component, type: string, toUp: Component[], opts
symb.em.log(type, { model: symb, toUp, context: 'symbols', opts }); symb.em.log(type, { model: symb, toUp, context: 'symbols', opts });
}; };
export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}) => { export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}): void => {
const changed = symbol.changedAttributes() || {}; const changed = symbol.dataResolverWatchers.getPropsDefsOrValues({ ...symbol.changedAttributes() });
const attrs = changed.attributes || {}; const attrs = symbol.dataResolverWatchers.getAttributesDefsOrValues({ ...changed.attributes });
delete changed.status;
delete changed.open; cleanChangedProperties(changed, attrs);
delete changed[keySymbols];
delete changed[keySymbol]; if (!isEmptyObj(changed)) {
delete changed[keySymbolOvrd]; const toUpdate = getSymbolsToUpdate(symbol, opts);
delete changed.attributes;
// Filter properties to propagate
filterPropertiesForPropagation(changed, symbol);
logSymbol(symbol, 'props', toUpdate, { opts, changed });
// Update child symbols
toUpdate.forEach((child) => {
const propsToUpdate = { ...changed };
filterPropertiesForPropagation(propsToUpdate, child);
child.set(propsToUpdate, { fromInstance: symbol, ...opts });
});
}
};
const cleanChangedProperties = (changed: Record<string, any>, attrs: Record<string, any>): void => {
const keysToDelete = ['status', 'open', keySymbols, keySymbol, keySymbolOvrd, 'attributes'];
keysToDelete.forEach((key) => delete changed[key]);
delete attrs.id; delete attrs.id;
if (!isEmptyObj(attrs)) { if (!isEmptyObj(attrs)) {
changed.attributes = attrs; changed.attributes = attrs;
} }
};
if (!isEmptyObj(changed)) { const filterPropertiesForPropagation = (props: Record<string, any>, component: Component): void => {
const toUp = getSymbolsToUpdate(symbol, opts); keys(props).forEach((prop) => {
// Avoid propagating overrides to other symbols if (!shouldPropagateProperty(props, prop, component)) {
keys(changed).map((prop) => { delete props[prop];
if (isSymbolOverride(symbol, prop)) delete changed[prop]; }
}); });
};
logSymbol(symbol, 'props', toUp, { opts, changed }); const shouldPropagateProperty = (props: Record<string, any>, prop: string, component: Component): boolean => {
toUp.forEach((child) => { const isCollectionVariableDefinition = (() => {
const propsChanged = { ...changed }; if (prop === 'attributes') {
// Avoid updating those with override const attributes = props['attributes'];
keys(propsChanged).map((prop) => { return Object.values(attributes).some((attr: any) => attr?.type === DataCollectionVariableType);
if (isSymbolOverride(child, prop)) delete propsChanged[prop]; }
});
child.set(propsChanged, { fromInstance: symbol, ...opts }); return props[prop]?.type === DataCollectionVariableType;
}); })();
}
return !isSymbolOverride(component, prop) || isCollectionVariableDefinition;
}; };
export const updateSymbolCls = (symbol: Component, opts: any = {}) => { export const updateSymbolCls = (symbol: Component, opts: any = {}) => {
@ -193,6 +217,9 @@ export const updateSymbolComps = (symbol: Component, m: Component, c: Components
toUp.forEach((rel) => { toUp.forEach((rel) => {
const relCmps = rel.components(); const relCmps = rel.components();
const toReset = cmps.map((cmp, i) => { const toReset = cmps.map((cmp, i) => {
if (symbol.get(keyCollectionDefinition)) {
return cmp.clone({ symbol: isSymbol(cmp) });
}
// This particular case here is to handle reset from `resetFromString` // This particular case here is to handle reset from `resetFromString`
// where we can receive an array of regulat components or already // where we can receive an array of regulat components or already
// existing symbols (updated already before reset) // existing symbols (updated already before reset)
@ -202,6 +229,7 @@ export const updateSymbolComps = (symbol: Component, m: Component, c: Components
} }
return relCmps.at(i); return relCmps.at(i);
}); });
relCmps.reset(toReset, { fromInstance: symbol, ...c } as any); relCmps.reset(toReset, { fromInstance: symbol, ...c } as any);
}); });
// Add // Add

6
packages/core/src/dom_components/model/types.ts

@ -1,3 +1,4 @@
import { DynamicWatchersOptions } from './ComponentResolverWatcher';
import Frame from '../../canvas/model/Frame'; import Frame from '../../canvas/model/Frame';
import { AddOptions, Nullable, OptionAsDocument } from '../../common'; import { AddOptions, Nullable, OptionAsDocument } from '../../common';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
@ -11,7 +12,7 @@ import Component from './Component';
import Components from './Components'; import Components from './Components';
import { ToolbarButtonProps } from './ToolbarButton'; import { ToolbarButtonProps } from './ToolbarButton';
import { ParseNodeOptions } from '../../parser/config/config'; import { ParseNodeOptions } from '../../parser/config/config';
import { DynamicValueDefinition } from '../../data_sources/types'; import { DataCollectionStateMap } from '../../data_sources/model/data_collection/types';
export type DragMode = 'translate' | 'absolute' | ''; export type DragMode = 'translate' | 'absolute' | '';
@ -253,7 +254,7 @@ export interface ComponentProperties {
[key: string]: any; [key: string]: any;
} }
export interface SymbolToUpOptions { export interface SymbolToUpOptions extends DynamicWatchersOptions {
changed?: string; changed?: string;
fromInstance?: boolean; fromInstance?: boolean;
noPropagate?: boolean; noPropagate?: boolean;
@ -321,4 +322,5 @@ export interface ComponentOptions {
frame?: Frame; frame?: Frame;
temporary?: boolean; temporary?: boolean;
avoidChildren?: boolean; avoidChildren?: boolean;
forCloning?: boolean;
} }

102
packages/core/src/domain_abstract/model/StyleableModel.ts

@ -1,30 +1,30 @@
import { isArray, isString, keys } from 'underscore'; import { isArray, isString, keys } from 'underscore';
import { Model, ObjectAny, ObjectHash, SetOptions, View } from '../../common'; import { Model, ObjectAny, ObjectHash, SetOptions } from '../../common';
import ParserHtml from '../../parser/model/ParserHtml'; import ParserHtml from '../../parser/model/ParserHtml';
import Selectors from '../../selector_manager/model/Selectors'; import Selectors from '../../selector_manager/model/Selectors';
import { shallowDiff } from '../../utils/mixins'; import { shallowDiff } from '../../utils/mixins';
import EditorModel from '../../editor/model/Editor'; import EditorModel from '../../editor/model/Editor';
import StyleDataVariable from '../../data_sources/model/StyleDataVariable'; import DataVariable, { DataVariableProps, DataVariableType } from '../../data_sources/model/DataVariable';
import { DataVariableDefinition, DataVariableType } from '../../data_sources/model/DataVariable'; import DataResolverListener from '../../data_sources/model/DataResolverListener';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
import CssRuleView from '../../css_composer/view/CssRuleView'; import CssRuleView from '../../css_composer/view/CssRuleView';
import ComponentView from '../../dom_components/view/ComponentView'; import ComponentView from '../../dom_components/view/ComponentView';
import Frame from '../../canvas/model/Frame'; import Frame from '../../canvas/model/Frame';
import { import {
DataCondition, DataCondition,
ConditionalVariableType, DataConditionType,
ConditionalVariableDefinition, DataConditionProps,
} from '../../data_sources/model/conditional_variables/DataCondition'; } from '../../data_sources/model/conditional_variables/DataCondition';
import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils'; import { isDataResolver, isDataResolverProps } from '../../data_sources/model/utils';
import { DynamicValueDefinition } from '../../data_sources/types'; import { DataResolverProps } from '../../data_sources/types';
export type StyleProps = Record<string, string | string[] | DataVariableDefinition | ConditionalVariableDefinition>;
export type UpdateStyleOptions = SetOptions & { export type StyleProps = Record<string, string | string[] | DataVariableProps | DataConditionProps>;
export interface UpdateStyleOptions extends SetOptions {
partial?: boolean; partial?: boolean;
addStyle?: StyleProps; addStyle?: StyleProps;
inline?: boolean; inline?: boolean;
noEvent?: boolean; noEvent?: boolean;
}; }
export type StyleableView = ComponentView | CssRuleView; export type StyleableView = ComponentView | CssRuleView;
@ -36,8 +36,8 @@ export const getLastStyleValue = (value: string | string[]) => {
export default class StyleableModel<T extends ObjectHash = any> extends Model<T> { export default class StyleableModel<T extends ObjectHash = any> extends Model<T> {
em?: EditorModel; em?: EditorModel;
dynamicVariableListeners: Record<string, DynamicVariableListenerManager> = {};
views: StyleableView[] = []; views: StyleableView[] = [];
styleResolverListeners: Record<string, DataResolverListener> = {};
constructor(attributes: T, options: { em?: EditorModel } = {}) { constructor(attributes: T, options: { em?: EditorModel } = {}) {
super(attributes, options); super(attributes, options);
@ -71,7 +71,7 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
const result: ObjectAny = { ...style }; const result: ObjectAny = { ...style };
if (this.em && !opts.skipResolve) { if (this.em && !opts.skipResolve) {
const resolvedStyle = this.resolveDataVariables({ ...result }); const resolvedStyle = this.getResolvedStyles({ ...result });
// @ts-ignore // @ts-ignore
return prop && isString(prop) ? resolvedStyle[prop] : resolvedStyle; return prop && isString(prop) ? resolvedStyle[prop] : resolvedStyle;
} }
@ -110,10 +110,12 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
} }
const styleValue = newStyle[key]; const styleValue = newStyle[key];
if (isDynamicValueDefinition(styleValue)) { if (isDataResolverProps(styleValue)) {
const styleDynamicVariable = this.resolveDynamicValue(styleValue); const dataResolver = this.getDataResolverInstance(styleValue);
newStyle[key] = styleDynamicVariable; if (dataResolver) {
this.manageDataVariableListener(styleDynamicVariable, key); newStyle[key] = dataResolver;
this.listenToDataResolver(dataResolver, key);
}
} }
}); });
@ -139,38 +141,33 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
return newStyle; return newStyle;
} }
private resolveDynamicValue(styleValue: DynamicValueDefinition) { private getDataResolverInstance(props: DataResolverProps) {
const dynamicType = styleValue.type; const em = this.em!;
let styleDynamicVariable; let resolver;
switch (dynamicType) {
switch (props.type) {
case DataVariableType: case DataVariableType:
styleDynamicVariable = new StyleDataVariable(styleValue, { em: this.em }); resolver = new DataVariable(props, { em });
break; break;
case ConditionalVariableType: { case DataConditionType: {
const { condition, ifTrue, ifFalse } = styleValue; const { condition, ifTrue, ifFalse } = props;
styleDynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em! }); resolver = new DataCondition(condition, ifTrue, ifFalse, { em });
break; break;
} }
default:
throw new Error(
`Unsupported dynamic value type for styles. Only '${DataVariableType}' and '${ConditionalVariableType}' are supported. Received '${dynamicType}'.`,
);
} }
return styleDynamicVariable; return resolver;
} }
/** listenToDataResolver(resolver: DataVariable | DataCondition, styleProp: string) {
* Manage DataVariableListenerManager for a style property const resolverListener = this.styleResolverListeners[styleProp];
*/ if (resolverListener) {
manageDataVariableListener(dataVar: StyleDataVariable | DataCondition, styleProp: string) { resolverListener.listenToResolver();
if (this.dynamicVariableListeners[styleProp]) {
this.dynamicVariableListeners[styleProp].listenToDynamicVariable();
} else { } else {
this.dynamicVariableListeners[styleProp] = new DynamicVariableListenerManager({ this.styleResolverListeners[styleProp] = new DataResolverListener({
em: this.em!, em: this.em!,
dataVariable: dataVar, resolver,
updateValueFromDataVariable: () => this.updateView(), onUpdate: () => this.updateView(),
}); });
} }
} }
@ -195,28 +192,29 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
this.views.forEach((view) => view.updateStyles()); this.views.forEach((view) => view.updateStyles());
} }
/** getResolvedStyles(style: StyleProps): StyleProps {
* Resolve dynamic values ( datasource variables - conditional variables ) to their actual values const resultStyle = { ...style };
*/
resolveDataVariables(style: StyleProps): StyleProps { keys(resultStyle).forEach((key) => {
const resolvedStyle = { ...style }; const styleValue = resultStyle[key];
keys(resolvedStyle).forEach((key) => {
const styleValue = resolvedStyle[key];
if (typeof styleValue === 'string' || Array.isArray(styleValue)) { if (typeof styleValue === 'string' || Array.isArray(styleValue)) {
return; return;
} }
if (isDynamicValueDefinition(styleValue)) { if (isDataResolverProps(styleValue)) {
const dataVar = this.resolveDynamicValue(styleValue); const resolver = this.getDataResolverInstance(styleValue);
resolvedStyle[key] = dataVar.getDataValue(); if (resolver) {
resultStyle[key] = resolver.getDataValue();
}
} }
if (isDynamicValue(styleValue)) { if (isDataResolver(styleValue)) {
resolvedStyle[key] = styleValue.getDataValue(); resultStyle[key] = styleValue.getDataValue();
} }
}); });
return resolvedStyle;
return resultStyle;
} }
/** /**

5
packages/core/src/trait_manager/model/Trait.ts

@ -1,4 +1,3 @@
import { ConditionalVariableType, DataCondition } from './../../data_sources/model/conditional_variables/DataCondition';
import { isString, isUndefined } from 'underscore'; import { isString, isUndefined } from 'underscore';
import Category from '../../abstract/ModuleCategory'; import Category from '../../abstract/ModuleCategory';
import { LocaleOptions, Model, SetOptions } from '../../common'; import { LocaleOptions, Model, SetOptions } from '../../common';
@ -8,8 +7,6 @@ import { isDef } from '../../utils/mixins';
import TraitsEvents, { TraitGetValueOptions, TraitOption, TraitProperties, TraitSetValueOptions } from '../types'; import TraitsEvents, { TraitGetValueOptions, TraitOption, TraitProperties, TraitSetValueOptions } from '../types';
import TraitView from '../view/TraitView'; import TraitView from '../view/TraitView';
import Traits from './Traits'; import Traits from './Traits';
import TraitDataVariable from '../../data_sources/model/TraitDataVariable';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
/** /**
* @property {String} id Trait id, eg. `my-trait-id`. * @property {String} id Trait id, eg. `my-trait-id`.
@ -29,8 +26,6 @@ export default class Trait extends Model<TraitProperties> {
em: EditorModel; em: EditorModel;
view?: TraitView; view?: TraitView;
el?: HTMLElement; el?: HTMLElement;
dynamicVariable?: TraitDataVariable | DataCondition;
dynamicVariableListener?: DynamicVariableListenerManager;
defaults() { defaults() {
return { return {

2
packages/core/src/utils/mixins.ts

@ -36,6 +36,8 @@ export const get = (object: ObjectAny, path: string | string[], def: any) => {
return (index && index == length ? object : undefined) ?? def; return (index && index == length ? object : undefined) ?? def;
}; };
export const serialize = (obj: ObjectAny) => JSON.parse(JSON.stringify(obj));
export const isBultInMethod = (key: string) => isFunction(obj[key]); export const isBultInMethod = (key: string) => isFunction(obj[key]);
export const normalizeKey = (key: string) => (isBultInMethod(key) ? `_${key}` : key); export const normalizeKey = (key: string) => (isBultInMethod(key) ? `_${key}` : key);

31
packages/core/test/specs/data_sources/model/conditional_variables/ComponentConditionalVariable.ts

@ -1,11 +1,12 @@
import { Component, DataSourceManager, Editor } from '../../../../../src'; import { Component, DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import {
import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; MissingConditionError,
DataConditionType,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import { DataSourceProps } from '../../../../../src/data_sources/types'; import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView';
import ConditionalComponentView from '../../../../../src/data_sources/view/ComponentDynamicView';
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import ComponentTableView from '../../../../../src/dom_components/view/ComponentTableView'; import ComponentTableView from '../../../../../src/dom_components/view/ComponentTableView';
import ComponentTextView from '../../../../../src/dom_components/view/ComponentTextView'; import ComponentTextView from '../../../../../src/dom_components/view/ComponentTextView';
@ -28,7 +29,7 @@ describe('ComponentConditionalVariable', () => {
it('should add a component with a condition that evaluates a component definition', () => { it('should add a component with a condition that evaluates a component definition', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: 0, left: 0,
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
@ -41,10 +42,10 @@ describe('ComponentConditionalVariable', () => {
}, },
})[0]; })[0];
expect(component).toBeDefined(); expect(component).toBeDefined();
expect(component.get('type')).toBe(ConditionalVariableType); expect(component.get('type')).toBe(DataConditionType);
expect(component.getInnerHTML()).toBe('<h1>some text</h1>'); expect(component.getInnerHTML()).toBe('<h1>some text</h1>');
const componentView = component.getView(); const componentView = component.getView();
expect(componentView).toBeInstanceOf(ConditionalComponentView); expect(componentView).toBeInstanceOf(ComponentDataConditionView);
expect(componentView?.el.textContent).toBe('some text'); expect(componentView?.el.textContent).toBe('some text');
const childComponent = getFirstChild(component); const childComponent = getFirstChild(component);
@ -58,7 +59,7 @@ describe('ComponentConditionalVariable', () => {
it('should add a component with a condition that evaluates a string', () => { it('should add a component with a condition that evaluates a string', () => {
const component = cmpRoot.append({ const component = cmpRoot.append({
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: 0, left: 0,
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
@ -67,10 +68,10 @@ describe('ComponentConditionalVariable', () => {
ifTrue: '<h1>some text</h1>', ifTrue: '<h1>some text</h1>',
})[0]; })[0];
expect(component).toBeDefined(); expect(component).toBeDefined();
expect(component.get('type')).toBe(ConditionalVariableType); expect(component.get('type')).toBe(DataConditionType);
expect(component.getInnerHTML()).toBe('<h1>some text</h1>'); expect(component.getInnerHTML()).toBe('<h1>some text</h1>');
const componentView = component.getView(); const componentView = component.getView();
expect(componentView).toBeInstanceOf(ConditionalComponentView); expect(componentView).toBeInstanceOf(ComponentDataConditionView);
expect(componentView?.el.textContent).toBe('some text'); expect(componentView?.el.textContent).toBe('some text');
const childComponent = getFirstChild(component); const childComponent = getFirstChild(component);
@ -93,7 +94,7 @@ describe('ComponentConditionalVariable', () => {
dsm.add(dataSource); dsm.add(dataSource);
const component = cmpRoot.append({ const component = cmpRoot.append({
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: { left: {
type: DataVariableType, type: DataVariableType,
@ -142,7 +143,7 @@ describe('ComponentConditionalVariable', () => {
dsm.add(dataSource); dsm.add(dataSource);
const component = cmpRoot.append({ const component = cmpRoot.append({
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: { left: {
type: DataVariableType, type: DataVariableType,
@ -158,7 +159,7 @@ describe('ComponentConditionalVariable', () => {
tagName: 'div', tagName: 'div',
components: [ components: [
{ {
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: { left: {
type: DataVariableType, type: DataVariableType,
@ -189,7 +190,7 @@ describe('ComponentConditionalVariable', () => {
it('should store conditional components', () => { it('should store conditional components', () => {
const conditionalCmptDef = { const conditionalCmptDef = {
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: 0, left: 0,
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
@ -215,7 +216,7 @@ describe('ComponentConditionalVariable', () => {
it('should throw an error if no condition is passed', () => { it('should throw an error if no condition is passed', () => {
const conditionalCmptDef = { const conditionalCmptDef = {
type: ConditionalVariableType, type: DataConditionType,
ifTrue: { ifTrue: {
tagName: 'h1', tagName: 'h1',
type: 'text', type: 'text',

11
packages/core/test/specs/data_sources/model/conditional_variables/ConditionalStyles.ts

@ -1,12 +1,11 @@
import { DataSourceManager, Editor } from '../../../../../src'; import { DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import { import {
ConditionalVariableType, DataConditionType,
MissingConditionError, MissingConditionError,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; 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 ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import EditorModel from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor';
import { filterObjectForSnapshot, setupTestEditor } from '../../../../common'; import { filterObjectForSnapshot, setupTestEditor } from '../../../../common';
@ -32,7 +31,7 @@ describe('StyleConditionalVariable', () => {
content: 'some text', content: 'some text',
style: { style: {
color: { color: {
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: 0, left: 0,
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
@ -64,7 +63,7 @@ describe('StyleConditionalVariable', () => {
content: 'some text', content: 'some text',
style: { style: {
color: { color: {
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: { left: {
type: DataVariableType, type: DataVariableType,
@ -96,7 +95,7 @@ describe('StyleConditionalVariable', () => {
content: 'some text', content: 'some text',
style: { style: {
color: { color: {
type: ConditionalVariableType, type: DataConditionType,
ifTrue: 'grey', ifTrue: 'grey',
ifFalse: 'red', ifFalse: 'red',
}, },
@ -112,7 +111,7 @@ describe('StyleConditionalVariable', () => {
content: 'some text', content: 'some text',
style: { style: {
color: { color: {
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: 0, left: 0,
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,

10
packages/core/test/specs/data_sources/model/conditional_variables/ConditionalTraits.ts

@ -1,5 +1,5 @@
import { DataSourceManager, Editor } from '../../../../../src'; import { DataSourceManager, Editor } from '../../../../../src';
import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; import { DataConditionType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper'; import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import EditorModel from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor';
@ -34,7 +34,7 @@ describe('conditional traits', () => {
label: 'Value', label: 'Value',
name: 'value', name: 'value',
value: { value: {
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: 0, left: 0,
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
@ -68,7 +68,7 @@ describe('conditional traits', () => {
name: 'value', name: 'value',
changeProp: true, changeProp: true,
value: { value: {
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: 0, left: 0,
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
@ -103,7 +103,7 @@ describe('conditional traits', () => {
name: 'value', name: 'value',
changeProp: true, changeProp: true,
value: { value: {
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: 0, left: 0,
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
@ -138,7 +138,7 @@ describe('conditional traits', () => {
name: 'value', name: 'value',
changeProp: true, changeProp: true,
value: { value: {
type: ConditionalVariableType, type: DataConditionType,
condition: { condition: {
left: 0, left: 0,
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,

39
packages/core/test/specs/data_sources/model/conditional_variables/DataCondition.ts

@ -1,15 +1,14 @@
import { DataSourceManager } from '../../../../../src'; import { DataSourceManager } from '../../../../../src';
import { import {
DataCondition, DataCondition,
ExpressionDefinition, ExpressionProps,
LogicGroupDefinition, LogicGroupProps,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition'; } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator'; import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
import { LogicalOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator'; import { LogicalOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/LogicalOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator'; import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperations'; import { StringOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/StringOperations';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable'; import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import { DataSourceProps } from '../../../../../src/data_sources/types';
import Editor from '../../../../../src/editor/model/Editor'; import Editor from '../../../../../src/editor/model/Editor';
import EditorModel from '../../../../../src/editor/model/Editor'; import EditorModel from '../../../../../src/editor/model/Editor';
@ -52,14 +51,14 @@ describe('DataCondition', () => {
describe('Operator Tests', () => { describe('Operator Tests', () => {
test('should evaluate using GenericOperation operators', () => { test('should evaluate using GenericOperation operators', () => {
const condition: ExpressionDefinition = { left: 5, operator: GenericOperation.equals, right: 5 }; const condition: ExpressionProps = { left: 5, operator: GenericOperation.equals, right: 5 };
const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal', { em }); const dataCondition = new DataCondition(condition, 'Equal', 'Not Equal', { em });
expect(dataCondition.getDataValue()).toBe('Equal'); expect(dataCondition.getDataValue()).toBe('Equal');
}); });
test('equals (false)', () => { test('equals (false)', () => {
const condition: ExpressionDefinition = { const condition: ExpressionProps = {
left: 'hello', left: 'hello',
operator: GenericOperation.equals, operator: GenericOperation.equals,
right: 'world', right: 'world',
@ -69,21 +68,21 @@ describe('DataCondition', () => {
}); });
test('should evaluate using StringOperation operators', () => { test('should evaluate using StringOperation operators', () => {
const condition: ExpressionDefinition = { left: 'apple', operator: StringOperation.contains, right: 'app' }; const condition: ExpressionProps = { left: 'apple', operator: StringOperation.contains, right: 'app' };
const dataCondition = new DataCondition(condition, 'Contains', "Doesn't contain", { em }); const dataCondition = new DataCondition(condition, 'Contains', "Doesn't contain", { em });
expect(dataCondition.getDataValue()).toBe('Contains'); expect(dataCondition.getDataValue()).toBe('Contains');
}); });
test('should evaluate using NumberOperation operators', () => { test('should evaluate using NumberOperation operators', () => {
const condition: ExpressionDefinition = { left: 10, operator: NumberOperation.lessThan, right: 15 }; const condition: ExpressionProps = { left: 10, operator: NumberOperation.lessThan, right: 15 };
const dataCondition = new DataCondition(condition, 'Valid', 'Invalid', { em }); const dataCondition = new DataCondition(condition, 'Valid', 'Invalid', { em });
expect(dataCondition.getDataValue()).toBe('Valid'); expect(dataCondition.getDataValue()).toBe('Valid');
}); });
test('should evaluate using LogicalOperation operators', () => { test('should evaluate using LogicalOperation operators', () => {
const logicGroup: LogicGroupDefinition = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: LogicalOperation.and,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: GenericOperation.equals, right: true },
@ -103,7 +102,7 @@ describe('DataCondition', () => {
}); });
test('should evaluate complex nested conditions', () => { test('should evaluate complex nested conditions', () => {
const nestedLogicGroup: LogicGroupDefinition = { const nestedLogicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.or, logicalOperator: LogicalOperation.or,
statements: [ statements: [
{ {
@ -124,7 +123,7 @@ describe('DataCondition', () => {
describe('LogicalGroup Tests', () => { describe('LogicalGroup Tests', () => {
test('should correctly handle AND logical operator', () => { test('should correctly handle AND logical operator', () => {
const logicGroup: LogicGroupDefinition = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: LogicalOperation.and,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: GenericOperation.equals, right: true },
@ -137,7 +136,7 @@ describe('DataCondition', () => {
}); });
test('should correctly handle OR logical operator', () => { test('should correctly handle OR logical operator', () => {
const logicGroup: LogicGroupDefinition = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.or, logicalOperator: LogicalOperation.or,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: false }, { left: true, operator: GenericOperation.equals, right: false },
@ -150,7 +149,7 @@ describe('DataCondition', () => {
}); });
test('should correctly handle XOR logical operator', () => { test('should correctly handle XOR logical operator', () => {
const logicGroup: LogicGroupDefinition = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.xor, logicalOperator: LogicalOperation.xor,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: GenericOperation.equals, right: true },
@ -164,7 +163,7 @@ describe('DataCondition', () => {
}); });
test('should handle nested logical groups', () => { test('should handle nested logical groups', () => {
const logicGroup: LogicGroupDefinition = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: LogicalOperation.and,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: GenericOperation.equals, right: true },
@ -183,7 +182,7 @@ describe('DataCondition', () => {
}); });
test('should handle groups with false conditions', () => { test('should handle groups with false conditions', () => {
const logicGroup: LogicGroupDefinition = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: LogicalOperation.and,
statements: [ statements: [
{ left: true, operator: GenericOperation.equals, right: true }, { left: true, operator: GenericOperation.equals, right: true },
@ -199,7 +198,7 @@ describe('DataCondition', () => {
describe('Conditions with dataVariables', () => { describe('Conditions with dataVariables', () => {
test('should return "Yes" when dataVariable matches expected value', () => { test('should return "Yes" when dataVariable matches expected value', () => {
const condition: ExpressionDefinition = { const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals, operator: GenericOperation.equals,
right: 'active', right: 'active',
@ -210,7 +209,7 @@ describe('DataCondition', () => {
}); });
test('should return "No" when dataVariable does not match expected value', () => { test('should return "No" when dataVariable does not match expected value', () => {
const condition: ExpressionDefinition = { const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.status' },
operator: GenericOperation.equals, operator: GenericOperation.equals,
right: 'inactive', right: 'inactive',
@ -222,7 +221,7 @@ describe('DataCondition', () => {
// TODO: unskip after adding UndefinedOperator // TODO: unskip after adding UndefinedOperator
test.skip('should handle missing data variable gracefully', () => { test.skip('should handle missing data variable gracefully', () => {
const condition: ExpressionDefinition = { const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.not_a_user.status' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.not_a_user.status' },
operator: GenericOperation.isDefined, operator: GenericOperation.isDefined,
right: undefined, right: undefined,
@ -233,7 +232,7 @@ describe('DataCondition', () => {
}); });
test('should correctly compare numeric values from dataVariables', () => { test('should correctly compare numeric values from dataVariables', () => {
const condition: ExpressionDefinition = { const condition: ExpressionProps = {
left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.age' }, left: { type: DataVariableType, path: 'USER_STATUS_SOURCE.USER_1.age' },
operator: NumberOperation.greaterThan, operator: NumberOperation.greaterThan,
right: 24, right: 24,
@ -249,7 +248,7 @@ describe('DataCondition', () => {
}; };
dsm.add(dataSource2); dsm.add(dataSource2);
const logicGroup: LogicGroupDefinition = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.and, logicalOperator: LogicalOperation.and,
statements: [ statements: [
{ {
@ -270,7 +269,7 @@ describe('DataCondition', () => {
}); });
test('should handle nested logical conditions with data variables', () => { test('should handle nested logical conditions with data variables', () => {
const logicGroup: LogicGroupDefinition = { const logicGroup: LogicGroupProps = {
logicalOperator: LogicalOperation.or, logicalOperator: LogicalOperation.or,
statements: [ statements: [
{ {

1004
packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.ts

File diff suppressed because it is too large

264
packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollectionVariable.ts

@ -0,0 +1,264 @@
import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import {
DataCollectionType,
DataCollectionVariableType,
} from '../../../../../src/data_sources/model/data_collection/constants';
import { DataCollectionStateVariableType } from '../../../../../src/data_sources/model/data_collection/types';
import EditorModel from '../../../../../src/editor/model/Editor';
import { ProjectData } from '../../../../../src/storage_manager';
import { setupTestEditor } from '../../../../common';
describe('Collection variable components', () => {
let em: EditorModel;
let editor: Editor;
let dsm: DataSourceManager;
let dataSource: DataSource;
let wrapper: Component;
let firstRecord: DataRecord;
let secondRecord: DataRecord;
beforeEach(() => {
({ em, editor, dsm } = setupTestEditor());
wrapper = em.getWrapper()!;
dataSource = dsm.add({
id: 'my_data_source_id',
records: [
{ id: 'user1', user: 'user1', age: '12' },
{ id: 'user2', user: 'user2', age: '14' },
{ id: 'user3', user: 'user3', age: '16' },
],
});
firstRecord = dataSource.getRecord('user1')!;
secondRecord = dataSource.getRecord('user2')!;
});
afterEach(() => {
em.destroy();
});
test('Gets the correct static value', async () => {
const cmp = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
components: [
{
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'my_collection',
path: 'user',
},
],
},
collectionConfig: {
collectionId: 'my_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0];
const firstGrandchild = cmp.components().at(0).components().at(0);
expect(firstGrandchild.getInnerHTML()).toContain('user1');
expect(firstGrandchild.getEl()?.innerHTML).toContain('user1');
const secondGrandchild = cmp.components().at(1).components().at(0);
expect(secondGrandchild.getInnerHTML()).toContain('user2');
expect(secondGrandchild.getEl()?.innerHTML).toContain('user2');
});
test('Watches collection variable changes', async () => {
const cmp = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
components: {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'my_collection',
path: 'user',
},
},
collectionConfig: {
collectionId: 'my_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0];
firstRecord.set('user', 'new_correct_value');
const firstGrandchild = cmp.components().at(0).components().at(0);
expect(firstGrandchild.getInnerHTML()).toContain('new_correct_value');
expect(firstGrandchild.getEl()?.innerHTML).toContain('new_correct_value');
const secondGrandchild = cmp.components().at(1).components().at(0);
expect(secondGrandchild.getInnerHTML()).toContain('user2');
expect(secondGrandchild.getEl()?.innerHTML).toContain('user2');
});
describe('Serialization', () => {
let cmp: Component;
beforeEach(() => {
const variableCmpDef = {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'my_collection',
path: 'user',
};
const collectionComponentDefinition = {
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
components: [
{
type: 'default',
},
variableCmpDef,
],
},
collectionConfig: {
collectionId: 'my_collection',
startIndex: 0,
endIndex: 2,
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
};
cmp = wrapper.components(collectionComponentDefinition)[0];
});
test('Serializion to JSON', () => {
expect(cmp.toJSON()).toMatchSnapshot(`Collection with collection variable component ( no grandchildren )`);
const firstChild = cmp.components().at(0);
const newChildDefinition = {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentIndex,
collectionId: 'my_collection',
path: 'user',
};
firstChild.components().at(0).components(newChildDefinition);
expect(cmp.toJSON()).toMatchSnapshot(`Collection with collection variable component ( with grandchildren )`);
});
test('Saving', () => {
const projectData = editor.getProjectData();
const page = projectData.pages[0];
const frame = page.frames[0];
const component = frame.component.components[0];
expect(component).toMatchSnapshot(`Collection with collection variable component ( no grandchildren )`);
const firstChild = cmp.components().at(0);
const newChildDefinition = {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentIndex,
collectionId: 'my_collection',
path: 'user',
};
firstChild.components().at(0).components(newChildDefinition);
expect(cmp.toJSON()).toMatchSnapshot(`Collection with collection variable component ( with grandchildren )`);
});
test('Loading', () => {
const componentProjectData: ProjectData = {
assets: [],
pages: [
{
frames: [
{
component: {
components: [
{
collectionDef: {
componentDef: {
type: 'default',
components: [
{
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'my_collection',
path: 'user',
},
],
},
collectionConfig: {
collectionId: 'my_collection',
dataSource: {
path: 'my_data_source_id',
type: DataVariableType,
},
endIndex: 1,
startIndex: 0,
},
},
type: DataCollectionType,
},
],
docEl: {
tagName: 'html',
},
head: {
type: 'head',
},
stylable: [
'background',
'background-color',
'background-image',
'background-repeat',
'background-attachment',
'background-position',
'background-size',
],
type: 'wrapper',
},
id: 'frameid',
},
],
id: 'pageid',
type: 'main',
},
],
styles: [],
symbols: [],
dataSources: [dataSource],
};
editor.loadProjectData(componentProjectData);
const components = editor.getComponents();
const component = components.models[0];
const firstChild = component.components().at(0);
const firstGrandchild = firstChild.components().at(0);
const secondChild = component.components().at(1);
const secondGrandchild = secondChild.components().at(0);
expect(firstGrandchild.getInnerHTML()).toBe('user1');
expect(secondGrandchild.getInnerHTML()).toBe('user2');
firstRecord.set('user', 'new_user1_value');
expect(firstGrandchild.getInnerHTML()).toBe('new_user1_value');
expect(secondGrandchild.getInnerHTML()).toBe('user2');
secondRecord.set('user', 'new_user2_value');
expect(firstGrandchild.getInnerHTML()).toBe('new_user1_value');
expect(secondGrandchild.getInnerHTML()).toBe('new_user2_value');
});
});
});

519
packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollection.ts.snap

@ -0,0 +1,519 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Collection component Serialization Saving: Collection with grandchildren 1`] = `
{
"collectionDef": {
"collectionConfig": {
"collectionId": "my_collection",
"dataSource": {
"path": "my_data_source_id",
"type": "data-variable",
},
"endIndex": 1,
"startIndex": 0,
},
"componentDef": {
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"components": [
{
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"components": [
{
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"type": "default",
},
],
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
{
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
],
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
},
"type": "data-collection",
}
`;
exports[`Collection component Serialization Saving: Collection with no grandchildren 1`] = `
{
"collectionDef": {
"collectionConfig": {
"collectionId": "my_collection",
"dataSource": {
"path": "my_data_source_id",
"type": "data-variable",
},
"endIndex": 1,
"startIndex": 0,
},
"componentDef": {
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"components": [
{
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
{
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
],
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
},
"type": "data-collection",
}
`;
exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with grandchildren 1`] = `
{
"collectionDef": {
"collectionConfig": {
"collectionId": "my_collection",
"dataSource": {
"path": "my_data_source_id",
"type": "data-variable",
},
"endIndex": 1,
"startIndex": 0,
},
"componentDef": {
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"components": [
{
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"components": [
{
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"type": "default",
},
],
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
{
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
],
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
},
"type": "data-collection",
}
`;
exports[`Collection component Serialization Serializion with Collection Variables to JSON: Collection with no grandchildren 1`] = `
{
"collectionDef": {
"collectionConfig": {
"collectionId": "my_collection",
"dataSource": {
"path": "my_data_source_id",
"type": "data-variable",
},
"endIndex": 1,
"startIndex": 0,
},
"componentDef": {
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"components": [
{
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
{
"attributes": {
"attribute_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
},
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
],
"custom_prop": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
"name": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"property_trait": {
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
},
"type": "data-collection",
}
`;

141
packages/core/test/specs/data_sources/model/data_collection/__snapshots__/ComponentDataCollectionVariable.ts.snap

@ -0,0 +1,141 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Collection variable components Serialization Saving: Collection with collection variable component ( no grandchildren ) 1`] = `
{
"collectionDef": {
"collectionConfig": {
"collectionId": "my_collection",
"dataSource": {
"path": "my_data_source_id",
"type": "data-variable",
},
"endIndex": 2,
"startIndex": 0,
},
"componentDef": {
"components": [
{
"type": "default",
},
{
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
],
"type": "default",
},
},
"type": "data-collection",
}
`;
exports[`Collection variable components Serialization Saving: Collection with collection variable component ( with grandchildren ) 1`] = `
{
"collectionDef": {
"collectionConfig": {
"collectionId": "my_collection",
"dataSource": {
"path": "my_data_source_id",
"type": "data-variable",
},
"endIndex": 2,
"startIndex": 0,
},
"componentDef": {
"components": [
{
"components": [
{
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
],
"type": "default",
},
{
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
],
"type": "default",
},
},
"type": "data-collection",
}
`;
exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( no grandchildren ) 1`] = `
{
"collectionDef": {
"collectionConfig": {
"collectionId": "my_collection",
"dataSource": {
"path": "my_data_source_id",
"type": "data-variable",
},
"endIndex": 2,
"startIndex": 0,
},
"componentDef": {
"components": [
{
"type": "default",
},
{
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
],
"type": "default",
},
},
"type": "data-collection",
}
`;
exports[`Collection variable components Serialization Serializion to JSON: Collection with collection variable component ( with grandchildren ) 1`] = `
{
"collectionDef": {
"collectionConfig": {
"collectionId": "my_collection",
"dataSource": {
"path": "my_data_source_id",
"type": "data-variable",
},
"endIndex": 2,
"startIndex": 0,
},
"componentDef": {
"components": [
{
"components": [
{
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentIndex",
},
],
"type": "default",
},
{
"collectionId": "my_collection",
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
],
"type": "default",
},
},
"type": "data-collection",
}
`;

36
packages/core/test/specs/data_sources/model/data_collection/__snapshots__/nestedComponentDataCollections.ts.snap

@ -0,0 +1,36 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Collection component Nested collections are correctly serialized 1`] = `
{
"collectionDef": {
"collectionConfig": {
"collectionId": "parent_collection",
"dataSource": {
"path": "my_data_source_id",
"type": "data-variable",
},
},
"componentDef": {
"collectionDef": {
"collectionConfig": {
"collectionId": "nested_collection",
"dataSource": {
"path": "nested_data_source_id",
"type": "data-variable",
},
},
"componentDef": {
"name": {
"path": "user",
"type": "data-collection-variable",
"variableType": "currentItem",
},
"type": "default",
},
},
"type": "data-collection",
},
},
"type": "data-collection",
}
`;

430
packages/core/test/specs/data_sources/model/data_collection/nestedComponentDataCollections.ts

@ -0,0 +1,430 @@
import { Component, DataRecord, DataSource, DataSourceManager, Editor } from '../../../../../src';
import { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import {
DataCollectionType,
DataCollectionVariableType,
} from '../../../../../src/data_sources/model/data_collection/constants';
import { DataCollectionStateVariableType } from '../../../../../src/data_sources/model/data_collection/types';
import EditorModel from '../../../../../src/editor/model/Editor';
import { setupTestEditor } from '../../../../common';
describe('Collection component', () => {
let em: EditorModel;
let editor: Editor;
let dsm: DataSourceManager;
let dataSource: DataSource;
let nestedDataSource: DataSource;
let wrapper: Component;
let firstRecord: DataRecord;
let secondRecord: DataRecord;
let firstNestedRecord: DataRecord;
let secondNestedRecord: DataRecord;
beforeEach(() => {
({ em, editor, dsm } = setupTestEditor());
wrapper = em.getWrapper()!;
dataSource = dsm.add({
id: 'my_data_source_id',
records: [
{ id: 'user1', user: 'user1', age: '12' },
{ id: 'user2', user: 'user2', age: '14' },
],
});
nestedDataSource = dsm.add({
id: 'nested_data_source_id',
records: [
{ id: 'nested_user1', user: 'nested_user1', age: '12' },
{ id: 'nested_user2', user: 'nested_user2', age: '14' },
{ id: 'nested_user3', user: 'nested_user3', age: '16' },
],
});
firstRecord = dataSource.getRecord('user1')!;
secondRecord = dataSource.getRecord('user2')!;
firstNestedRecord = nestedDataSource.getRecord('nested_user1')!;
secondNestedRecord = nestedDataSource.getRecord('nested_user2')!;
});
afterEach(() => {
em.destroy();
});
test('Nested collections bind to correct data sources', () => {
const parentCollection = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
name: {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'nested_collection',
path: 'user',
},
},
collectionConfig: {
collectionId: 'nested_collection',
dataSource: {
type: DataVariableType,
path: 'nested_data_source_id',
},
},
},
},
collectionConfig: {
collectionId: 'parent_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0];
const nestedCollection = parentCollection.components().at(0);
const nestedFirstChild = nestedCollection.components().at(0);
const nestedSecondChild = nestedCollection.components().at(1);
expect(nestedFirstChild.get('name')).toBe('nested_user1');
expect(nestedSecondChild.get('name')).toBe('nested_user2');
});
test('Updates in parent collection propagate to nested collections', () => {
const parentCollection = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
name: {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'nested_collection',
path: 'user',
},
},
collectionConfig: {
collectionId: 'nested_collection',
dataSource: {
type: DataVariableType,
path: 'nested_data_source_id',
},
},
},
},
collectionConfig: {
collectionId: 'parent_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0];
const nestedCollection = parentCollection.components().at(0);
const nestedFirstChild = nestedCollection.components().at(0);
const nestedSecondChild = nestedCollection.components().at(1);
firstNestedRecord.set('user', 'updated_user1');
expect(nestedFirstChild.get('name')).toBe('updated_user1');
expect(nestedSecondChild.get('name')).toBe('nested_user2');
});
test('Nested collections are correctly serialized', () => {
const parentCollection = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
name: {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
path: 'user',
},
},
collectionConfig: {
collectionId: 'nested_collection',
dataSource: {
type: DataVariableType,
path: 'nested_data_source_id',
},
},
},
},
collectionConfig: {
collectionId: 'parent_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0];
const serialized = parentCollection.toJSON();
expect(serialized).toMatchSnapshot();
});
test('Nested collections respect startIndex and endIndex', () => {
const parentCollection = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
name: {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'nested_collection',
path: 'user',
},
},
collectionConfig: {
collectionId: 'nested_collection',
startIndex: 0,
endIndex: 1,
dataSource: {
type: DataVariableType,
path: 'nested_data_source_id',
},
},
},
},
collectionConfig: {
collectionId: 'parent_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0];
const nestedCollection = parentCollection.components().at(0);
expect(nestedCollection.components().length).toBe(2);
});
test('Nested collection gets and watches value from the parent collection', () => {
const parentCollection = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
name: {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'parent_collection',
path: 'user',
},
},
collectionConfig: {
collectionId: 'nested_collection',
dataSource: {
type: DataVariableType,
path: 'nested_data_source_id',
},
},
},
},
collectionConfig: {
collectionId: 'parent_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0];
const nestedCollection = parentCollection.components().at(0);
const firstNestedChild = nestedCollection.components().at(0);
// Verify initial value
expect(firstNestedChild.get('name')).toBe('user1');
// Update value in parent collection and verify nested collection updates
firstRecord.set('user', 'updated_user1');
expect(firstNestedChild.get('name')).toBe('updated_user1');
});
test('Nested collection switches to using its own collection variable', () => {
const parentCollection = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
name: {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
path: 'user',
collectionId: 'parent_collection',
},
},
collectionConfig: {
collectionId: 'nested_collection',
dataSource: {
type: DataVariableType,
path: 'nested_data_source_id',
},
},
},
},
collectionConfig: {
collectionId: 'parent_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0];
const nestedCollection = parentCollection.components().at(0);
const firstChild = nestedCollection.components().at(0);
// Replace the collection variable with one from the inner collection
firstChild.set('name', {
// @ts-ignore
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
path: 'user',
collectionId: 'nested_collection',
});
expect(firstChild.get('name')).toBe('nested_user1');
});
describe('Nested Collection Component with Parent and Nested Data Sources', () => {
let parentCollection: Component;
let nestedCollection: Component;
beforeEach(() => {
// Initialize the parent and nested collections
parentCollection = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: DataCollectionType,
name: {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'parent_collection',
path: 'user',
},
collectionDef: {
componentDef: {
type: 'default',
name: {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'nested_collection',
path: 'user',
},
},
collectionConfig: {
collectionId: 'nested_collection',
dataSource: {
type: DataVariableType,
path: 'nested_data_source_id',
},
},
},
},
collectionConfig: {
collectionId: 'parent_collection',
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0];
nestedCollection = parentCollection.components().at(0);
});
test('Removing a record from the parent data source updates the parent collection correctly', () => {
// Verify initial state
expect(parentCollection.components().length).toBe(2); // 2 parent records initially
// Remove a record from the parent data source
dataSource.removeRecord('user1');
// Verify that the parent collection updates correctly
expect(parentCollection.components().length).toBe(1); // Only 1 parent record remains
expect(parentCollection.components().at(0).get('name')).toBe('user2'); // Verify updated name
// Verify that the nested collection is unaffected
expect(nestedCollection.components().length).toBe(3); // Nested records remain the same
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify nested name
});
test('Adding a record to the parent data source updates the parent collection correctly', () => {
// Verify initial state
expect(parentCollection.components().length).toBe(2); // 2 parent records initially
// Add a new record to the parent data source
dataSource.addRecord({ id: 'user3', user: 'user3', age: '16' });
// Verify that the parent collection updates correctly
expect(parentCollection.components().length).toBe(3); // 3 parent records now
expect(parentCollection.components().at(2).get('name')).toBe('user3'); // Verify new name
// Verify that the nested collection is unaffected
expect(nestedCollection.components().length).toBe(3); // Nested records remain the same
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify nested name
expect(parentCollection.components().at(2).components().at(0).get('name')).toBe('nested_user1'); // Verify nested name
});
test('Removing a record from the nested data source updates the nested collection correctly', () => {
// Verify initial state
expect(nestedCollection.components().length).toBe(3); // 3 nested records initially
// Remove a record from the nested data source
nestedDataSource.removeRecord('nested_user1');
// Verify that the nested collection updates correctly
expect(nestedCollection.components().length).toBe(2); // Only 2 nested records remain
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user2'); // Verify updated name
expect(nestedCollection.components().at(1).get('name')).toBe('nested_user3'); // Verify updated name
});
test('Adding a record to the nested data source updates the nested collection correctly', () => {
// Verify initial state
expect(nestedCollection.components().length).toBe(3); // 3 nested records initially
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify initial name
expect(nestedCollection.components().at(1).get('name')).toBe('nested_user2'); // Verify initial name
expect(nestedCollection.components().at(2).get('name')).toBe('nested_user3'); // Verify initial name
// Add a new record to the nested data source
nestedDataSource.addRecord({ id: 'user4', user: 'nested_user4', age: '18' });
// Verify that the nested collection updates correctly
expect(nestedCollection.components().length).toBe(4); // 4 nested records now
expect(nestedCollection.components().at(3).get('name')).toBe('nested_user4'); // Verify new name
// Verify existing records are unaffected
expect(nestedCollection.components().at(0).get('name')).toBe('nested_user1'); // Verify existing name
expect(nestedCollection.components().at(1).get('name')).toBe('nested_user2'); // Verify existing name
expect(nestedCollection.components().at(2).get('name')).toBe('nested_user3'); // Verify existing name
});
});
});
Loading…
Cancel
Save