Browse Source

Add collection components (#6359)

pull/6398/head
mohamed yahia 11 months 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 EditorModel from '../../editor/model/Editor';
import CssRuleView from '../view/CssRuleView';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
/** @private */
export interface CssRuleProperties {
@ -95,7 +94,6 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
em?: EditorModel;
opt: any;
views: CssRuleView[] = [];
dynamicVariableListeners: Record<string, DynamicVariableListenerManager> = {};
defaults() {
return {

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

@ -1,8 +1,11 @@
import Component from '../../dom_components/model/Component';
import { ComponentOptions } from '../../dom_components/model/types';
import { toLowerCase } from '../../utils/mixins';
import { DataVariableType } from './DataVariable';
import DataVariable, { DataVariableProps, DataVariableType } from './DataVariable';
export default class ComponentDataVariable extends Component {
dataResolver: DataVariable;
get defaults() {
return {
// @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() {
const { path, defaultValue } = this.attributes;
return this.em.DataSources.getValue(path, defaultValue);
return this.dataResolver.getDataValue();
}
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 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 DataRecords from './DataRecords';
import DataSources from './DataSources';

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

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

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 { TraitProperties } from '../../trait_manager/types';
export type TraitDataVariableDefinition = TraitProperties & DataVariableDefinition;
export interface TraitDataVariableProps extends Omit<TraitProperties, 'type'>, DataVariableProps {}
export default class TraitDataVariable extends DataVariable {
trait?: Trait;
constructor(attrs: TraitDataVariableDefinition, options: any) {
super(attrs, options);
constructor(props: TraitDataVariableProps, options: any) {
super(props, options);
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 { evaluateVariable, isDataVariable } from '../utils';
import { ExpressionDefinition, LogicGroupDefinition } from './DataCondition';
import { ExpressionProps, LogicGroupProps } from './DataCondition';
import { LogicalGroupStatement } from './LogicalGroupStatement';
import { Operator } from './operators';
import { GenericOperation, GenericOperator } from './operators/GenericOperator';
@ -10,13 +10,15 @@ import { NumberOperator, NumberOperation } from './operators/NumberOperator';
import { StringOperator, StringOperation } from './operators/StringOperations';
import { Model } from '../../../common';
export type ConditionProps = ExpressionProps | LogicGroupProps | boolean;
export class Condition extends Model {
private condition: ExpressionDefinition | LogicGroupDefinition | boolean;
private condition: ConditionProps;
private em: EditorModel;
constructor(condition: ExpressionDefinition | LogicGroupDefinition | boolean, opts: { em: EditorModel }) {
super(condition);
this.condition = condition;
constructor(props: ConditionProps, opts: { em: EditorModel }) {
super(props);
this.condition = props;
this.em = opts.em;
}
@ -27,7 +29,7 @@ export class Condition extends Model {
/**
* Recursively evaluates conditions and logic groups.
*/
private evaluateCondition(condition: any): boolean {
private evaluateCondition(condition: ConditionProps): boolean {
if (typeof condition === 'boolean') return condition;
if (this.isLogicGroup(condition)) {
@ -68,7 +70,7 @@ export class Condition extends Model {
* Extracts all data variables from the condition, including nested ones.
*/
getDataVariables() {
const variables: DataVariableDefinition[] = [];
const variables: DataVariableProps[] = [];
this.extractVariables(this.condition, variables);
return variables;
}
@ -76,10 +78,7 @@ export class Condition extends Model {
/**
* Recursively extracts variables from expressions or logic groups.
*/
private extractVariables(
condition: boolean | LogicGroupDefinition | ExpressionDefinition,
variables: DataVariableDefinition[],
): void {
private extractVariables(condition: ConditionProps, variables: DataVariableProps[]): void {
if (this.isExpression(condition)) {
if (isDataVariable(condition.left)) variables.push(condition.left);
if (isDataVariable(condition.right)) variables.push(condition.right);
@ -91,14 +90,14 @@ export class Condition extends Model {
/**
* 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);
}
/**
* 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';
}

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 { LogicalOperation } from './operators/LogicalOperator';
import DynamicVariableListenerManager from '../DataVariableListenerManager';
import EditorModel from '../../../editor/model/Editor';
import { Condition } from './Condition';
import DataVariable, { DataVariableDefinition } from '../DataVariable';
import DataVariable, { DataVariableProps } from '../DataVariable';
import DataResolverListener from '../DataResolverListener';
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 type ExpressionDefinition = {
export interface ExpressionProps {
left: any;
operator: GenericOperation | StringOperation | NumberOperation;
right: any;
};
}
export type LogicGroupDefinition = {
export interface LogicGroupProps {
logicalOperator: LogicalOperation;
statements: (ExpressionDefinition | LogicGroupDefinition | boolean)[];
};
statements: ConditionProps[];
}
export type ConditionDefinition = ExpressionDefinition | LogicGroupDefinition | boolean;
export type ConditionalVariableDefinition = {
type: typeof ConditionalVariableType;
condition: ConditionDefinition;
export interface DataConditionProps {
type: typeof DataConditionType;
condition: ConditionProps;
ifTrue: any;
ifFalse: any;
};
}
type DataConditionType = {
type: typeof ConditionalVariableType;
interface DataConditionPropsDefined extends Omit<DataConditionProps, 'condition'> {
condition: Condition;
ifTrue: any;
ifFalse: any;
};
export class DataCondition extends Model<DataConditionType> {
}
export class DataCondition extends Model<DataConditionPropsDefined> {
lastEvaluationResult: boolean;
private condition: Condition;
private em: EditorModel;
private variableListeners: DynamicVariableListenerManager[] = [];
private resolverListeners: DataResolverListener[] = [];
private _onValueChange?: () => void;
constructor(
condition: ExpressionDefinition | LogicGroupDefinition | boolean,
condition: ConditionProps,
public ifTrue: any,
public ifFalse: any,
opts: { em: EditorModel; onValueChange?: () => void },
@ -54,18 +51,21 @@ export class DataCondition extends Model<DataConditionType> {
const conditionInstance = new Condition(condition, { em: opts.em });
super({
type: ConditionalVariableType,
type: DataConditionType,
condition: conditionInstance,
ifTrue,
ifFalse,
});
this.condition = conditionInstance;
this.em = opts.em;
this.lastEvaluationResult = this.evaluate();
this.listenToDataVariables();
this._onValueChange = opts.onValueChange;
}
get condition() {
return this.get('condition')!;
}
evaluate() {
return this.condition.evaluate();
}
@ -84,7 +84,8 @@ export class DataCondition extends Model<DataConditionType> {
}
private listenToDataVariables() {
if (!this.em) return;
const { em } = this;
if (!em) return;
// Clear previous listeners to avoid memory leaks
this.cleanupListeners();
@ -92,22 +93,21 @@ export class DataCondition extends Model<DataConditionType> {
const dataVariables = this.getDependentDataVariables();
dataVariables.forEach((variable) => {
const variableInstance = new DataVariable(variable, { em: this.em });
const listener = new DynamicVariableListenerManager({
em: this.em!,
dataVariable: variableInstance,
updateValueFromDataVariable: (() => {
const listener = new DataResolverListener({
em,
resolver: new DataVariable(variable, { em: this.em }),
onUpdate: (() => {
this.reevaluate();
this._onValueChange?.();
}).bind(this),
});
this.variableListeners.push(listener);
this.resolverListeners.push(listener);
});
}
getDependentDataVariables() {
const dataVariables: DataVariableDefinition[] = this.condition.getDataVariables();
const dataVariables: DataVariableProps[] = this.condition.getDataVariables();
if (isDataVariable(this.ifTrue)) dataVariables.push(this.ifTrue);
if (isDataVariable(this.ifFalse)) dataVariables.push(this.ifFalse);
@ -115,13 +115,13 @@ export class DataCondition extends Model<DataConditionType> {
}
private cleanupListeners() {
this.variableListeners.forEach((listener) => listener.destroy());
this.variableListeners = [];
this.resolverListeners.forEach((listener) => listener.destroy());
this.resolverListeners = [];
}
toJSON() {
return {
type: ConditionalVariableType,
type: DataConditionType,
condition: this.condition,
ifTrue: this.ifTrue,
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 { Condition, ConditionProps } from './Condition';
import { LogicalOperator } from './operators/LogicalOperator';
export class LogicalGroupStatement {
private em: EditorModel;
constructor(
private operator: LogicalOperator,
private statements: (ExpressionDefinition | LogicGroupDefinition | boolean)[],
private statements: ConditionProps[],
opts: { em: EditorModel },
) {
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':
return typeof left === 'boolean';
case 'isDefaultValue':
return left instanceof DataVariable && left.get('default') === right;
return left instanceof DataVariable && left.get('defaultValue') === right;
default:
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 { DynamicValue, DynamicValueDefinition } from '../types';
import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition';
import DataVariable, { DataVariableType } from './DataVariable';
import { DataResolver, DataResolverProps } from '../types';
import { DataCollectionStateMap } from './data_collection/types';
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 {
return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value?.type);
export function isDataResolverProps(value: any): value is DataResolverProps {
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;
}
export function isDataVariable(variable: any) {
export function isDataVariable(variable: any): variable is DataVariableProps {
return variable?.type === DataVariableType;
}
export function isDataCondition(variable: any) {
return variable?.type === ConditionalVariableType;
return variable?.type === DataConditionType;
}
export function evaluateVariable(variable: any, em: EditorModel) {
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable;
}
export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, em: EditorModel): DynamicValue {
const dynamicType = valueDefinition.type;
let dynamicVariable: DynamicValue;
export function getDataResolverInstance(
resolverProps: DataResolverProps,
options: { em: EditorModel; collectionsStateMap?: DataCollectionStateMap },
): DataResolver {
const { type } = resolverProps;
let resolver: DataResolver;
switch (dynamicType) {
switch (type) {
case DataVariableType:
dynamicVariable = new DataVariable(valueDefinition, { em: em });
resolver = new DataVariable(resolverProps, options);
break;
case ConditionalVariableType: {
const { condition, ifTrue, ifFalse } = valueDefinition;
dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: em });
case DataConditionType: {
const { condition, ifTrue, ifFalse } = resolverProps;
resolver = new DataCondition(condition, ifTrue, ifFalse, options);
break;
}
case DataCollectionVariableType: {
resolver = new DataCollectionVariable(resolverProps, options);
break;
}
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) {
const dynamicVariable = getDynamicValueInstance(valueDefinition, em);
export function getDataResolverInstanceValue(
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 ComponentDataVariable from './model/ComponentDataVariable';
import { Model, Collection, ObjectAny } from '../common';
import DataCollectionVariable from './model/data_collection/DataCollectionVariable';
import { DataCollectionVariableProps } from './model/data_collection/types';
import DataRecord from './model/DataRecord';
import DataRecords from './model/DataRecords';
import DataVariable, { DataVariableDefinition } from './model/DataVariable';
import { ConditionalVariableDefinition, DataCondition } from './model/conditional_variables/DataCondition';
import DataVariable, { DataVariableProps } from './model/DataVariable';
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 {
/**
* Record id.
@ -21,8 +24,8 @@ export interface DataRecordProps extends ObjectAny {
[key: string]: any;
}
export interface DataVariableListener {
obj: any;
export interface DataSourceListener {
obj: Model | Collection;
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 ComponentDataVariable from '../model/ComponentDataVariable';
import DynamicVariableListenerManager from '../model/DataVariableListenerManager';
import DataResolverListener from '../model/DataResolverListener';
export default class ComponentDataVariableView extends ComponentView<ComponentDataVariable> {
dynamicVariableListener?: DynamicVariableListenerManager;
dataResolverListener!: DataResolverListener;
initialize(opt = {}) {
super.initialize(opt);
this.dynamicVariableListener = new DynamicVariableListenerManager({
em: this.em!,
dataVariable: this.model,
updateValueFromDataVariable: () => this.postRender(),
this.dataResolverListener = new DataResolverListener({
em: this.em,
resolver: this.model.dataResolver,
onUpdate: () => this.postRender(),
});
}
remove() {
this.dataResolverListener.destroy();
return super.remove();
}
postRender() {
const { model, el, em } = this;
const { path, defaultValue } = model.attributes;
el.innerHTML = em.DataSources.getValue(path, defaultValue);
this.el.innerHTML = this.model.getDataValue();
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
*/
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 { ObjectAny } from '../common';
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 ComponentDataVariableView from '../data_sources/view/ComponentDataVariableView';
import { DataVariableType } from '../data_sources/model/DataVariable';
import { ConditionalVariableType } from '../data_sources/model/conditional_variables/DataCondition';
import ComponentConditionalVariable from '../data_sources/model/conditional_variables/ConditionalComponent';
import ConditionalComponentView from '../data_sources/view/ComponentDynamicView';
import { DataConditionType } from '../data_sources/model/conditional_variables/DataCondition';
import ComponentDataCondition from '../data_sources/model/conditional_variables/ComponentDataCondition';
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 =
| 'component:create'
@ -194,9 +199,19 @@ export interface CanMoveResult {
export default class ComponentManager extends ItemManagerModule<DomComponentsConfig, any> {
componentTypes: ComponentStackItem[] = [
{
id: ConditionalVariableType,
model: ComponentConditionalVariable,
view: ConditionalComponentView,
id: DataCollectionVariableType,
model: ComponentDataCollectionVariable,
view: ComponentDataCollectionVariableView,
},
{
id: DataCollectionType,
model: ComponentDataCollection,
view: ComponentDataCollectionView,
},
{
id: DataConditionType,
model: ComponentDataCondition,
view: ComponentDataConditionView,
},
{
id: DataVariableType,

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

@ -51,15 +51,11 @@ import {
updateSymbolComps,
updateSymbolProps,
} from './SymbolUtils';
import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher';
import { DynamicValueWatcher } from './DynamicValueWatcher';
import { DynamicValueDefinition } from '../../data_sources/types';
import { ComponentDataResolverWatchers } from './ComponentDataResolverWatchers';
import { DynamicWatchersOptions } from './ComponentResolverWatcher';
import { keyIsCollectionItem, keyCollectionsStateMap } from '../../data_sources/model/data_collection/constants';
export interface IComponent extends ExtractMethods<Component> {}
export interface DynamicWatchersOptions {
skipWatcherUpdates?: boolean;
fromDataSource?: boolean;
}
export interface SetAttrOptions extends SetOptions, UpdateStyleOptions, DynamicWatchersOptions {}
export interface ComponentSetOptions extends SetOptions, DynamicWatchersOptions {}
@ -262,12 +258,20 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @private
* @ts-ignore */
collection!: Components;
componentDVListener: ComponentDynamicValueWatcher;
collectionStateListeners: string[] = [];
dataResolverWatchers: ComponentDataResolverWatchers;
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
this.componentDVListener = new ComponentDynamicValueWatcher(this, opt.em);
this.componentDVListener.addProps(props);
const dataResolverWatchers = new ComponentDataResolverWatchers(undefined, {
em: opt.em,
collectionsStateMap: props[keyCollectionsStateMap],
});
super(props, {
...opt,
dataResolverWatchers,
} as any);
dataResolverWatchers.bindComponent(this);
this.dataResolverWatchers = dataResolverWatchers;
bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps');
const em = opt.em;
@ -295,9 +299,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.opt = opt;
this.em = em!;
this.config = opt.config || {};
const dynamicAttributes = this.dataResolverWatchers.getDynamicAttributesDefs();
this.setAttributes({
...(result(this, 'defaults').attributes || {}),
...(this.get('attributes') || {}),
...dynamicAttributes,
});
this.ccid = Component.createId(this, opt);
this.preInit();
@ -343,7 +349,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
optionsOrUndefined?: ComponentSetOptions,
): this {
let attributes: Partial<ComponentProperties>;
let options: ComponentSetOptions = { skipWatcherUpdates: false, fromDataSource: false };
let options: ComponentSetOptions & {
dataResolverWatchers?: ComponentDataResolverWatchers;
} = { skipWatcherUpdates: false, fromDataSource: false };
if (typeof keyOrAttributes === 'object') {
attributes = keyOrAttributes;
options = valueOrOptions || (options as ComponentSetOptions);
@ -355,16 +363,10 @@ export default class Component extends StyleableModel<ComponentProperties> {
options = optionsOrUndefined || options;
}
// @ts-ignore
const em = this.em || options.em;
const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attributes, em);
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource;
if (!shouldSkipWatcherUpdates) {
this.componentDVListener?.addProps(attributes);
}
this.dataResolverWatchers = this.dataResolverWatchers || options.dataResolverWatchers;
const evaluatedProps = this.dataResolverWatchers.addProps(attributes, options);
return super.set(evaluatedAttributes, options);
return super.set(evaluatedProps, options);
}
__postAdd(opts: { recursive?: boolean } = {}) {
@ -503,8 +505,13 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @example
* component.setSymbolOverride(['children', 'classes']);
*/
setSymbolOverride(value?: boolean | string | string[]) {
this.set(keySymbolOvrd, (isString(value) ? [value] : value) ?? 0);
setSymbolOverride(value: boolean | string | string[], options: DynamicWatchersOptions = {}) {
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' });
*/
setAttributes(attrs: ObjectAny, opts: SetAttrOptions = { skipWatcherUpdates: false, fromDataSource: false }) {
// @ts-ignore
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);
this.set('attributes', { ...attrs }, opts);
return this;
}
@ -706,7 +706,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* component.addAttributes({ 'data-key': 'value' });
*/
addAttributes(attrs: ObjectAny, opts: SetAttrOptions = {}) {
const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs();
const dynamicAttributes = this.dataResolverWatchers.getDynamicAttributesDefs();
return this.setAttributes(
{
...this.getAttributes({ noClass: true }),
@ -728,7 +728,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
removeAttributes(attrs: string | string[] = [], opts: SetOptions = {}) {
const attrArr = Array.isArray(attrs) ? attrs : [attrs];
this.componentDVListener.removeAttributes(attrArr);
this.dataResolverWatchers.removeAttributes(attrArr);
const compAttr = this.getAttributes();
attrArr.map((i) => delete compAttr[i]);
@ -965,12 +965,12 @@ export default class Component extends StyleableModel<ComponentProperties> {
const value = trait.getInitValue();
if (trait.changeProp) {
this.set(name, value);
isUndefined(this.get(name)) && this.set(name, value);
} else {
if (name && value) attrs[name] = value;
}
});
const dynamicAttributes = this.componentDVListener.getDynamicAttributesDefs();
const dynamicAttributes = this.dataResolverWatchers.getDynamicAttributesDefs();
traits.length &&
this.setAttributes({
...attrs,
@ -1318,13 +1318,14 @@ export default class Component extends StyleableModel<ComponentProperties> {
clone(opt: { symbol?: boolean; symbolInv?: boolean } = {}): this {
const em = this.em;
const attr = {
...this.componentDVListener.getPropsDefsOrValues(this.attributes),
...this.attributes,
...this.dataResolverWatchers.getDynamicPropsDefs(),
};
const opts = { ...this.opt };
const id = this.getId();
const cssc = em?.Css;
attr.attributes = {
...(attr.attributes ? this.componentDVListener.getAttributesDefsOrValues(attr.attributes) : undefined),
...(attr.attributes ? this.dataResolverWatchers.getAttributesDefsOrValues(attr.attributes) : undefined),
};
// @ts-ignore
attr.components = [];
@ -1353,6 +1354,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
attr.status = '';
// @ts-ignore
opts.collection = null;
opts.forCloning = true;
// @ts-ignore
const cloned = new this.constructor(attr, opts);
@ -1581,9 +1583,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
toJSON(opts: ObjectAny = {}): ComponentDefinition {
let obj = Model.prototype.toJSON.call(this, opts);
obj = { ...obj, ...this.componentDVListener.getDynamicPropsDefs() };
obj.attributes = this.componentDVListener.getAttributesDefsOrValues(this.getAttributes());
delete obj.componentDVListener;
obj = { ...obj, ...this.dataResolverWatchers.getDynamicPropsDefs() };
obj.attributes = this.dataResolverWatchers.getAttributesDefsOrValues(this.getAttributes());
delete obj.dataResolverWatchers;
delete obj.attributes.class;
delete obj.toolbar;
delete obj.traits;
@ -1591,6 +1593,14 @@ export default class Component extends StyleableModel<ComponentProperties> {
delete obj.open; // used in Layers
delete obj._undoexc;
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) {
const symbol = obj[keySymbol];
@ -1657,9 +1667,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
* @return {this}
*/
setId(id: string, opts?: SetOptions & { idUpdate?: boolean }) {
const attrs = { ...this.get('attributes') };
attrs.id = id;
this.set('attributes', attrs, opts);
this.addAttributes({ id }, opts);
return this;
}
@ -1822,7 +1830,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
}
destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR {
this.componentDVListener.destroy();
this.dataResolverWatchers.destroy();
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 { isEmptyObj } from '../../utils/mixins';
import Components from './Components';
import {
DataCollectionVariableType,
keyCollectionDefinition,
} from '../../data_sources/model/data_collection/constants';
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 });
};
export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}) => {
const changed = symbol.changedAttributes() || {};
const attrs = changed.attributes || {};
delete changed.status;
delete changed.open;
delete changed[keySymbols];
delete changed[keySymbol];
delete changed[keySymbolOvrd];
delete changed.attributes;
export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}): void => {
const changed = symbol.dataResolverWatchers.getPropsDefsOrValues({ ...symbol.changedAttributes() });
const attrs = symbol.dataResolverWatchers.getAttributesDefsOrValues({ ...changed.attributes });
cleanChangedProperties(changed, attrs);
if (!isEmptyObj(changed)) {
const toUpdate = getSymbolsToUpdate(symbol, opts);
// 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;
if (!isEmptyObj(attrs)) {
changed.attributes = attrs;
}
};
if (!isEmptyObj(changed)) {
const toUp = getSymbolsToUpdate(symbol, opts);
// Avoid propagating overrides to other symbols
keys(changed).map((prop) => {
if (isSymbolOverride(symbol, prop)) delete changed[prop];
});
const filterPropertiesForPropagation = (props: Record<string, any>, component: Component): void => {
keys(props).forEach((prop) => {
if (!shouldPropagateProperty(props, prop, component)) {
delete props[prop];
}
});
};
logSymbol(symbol, 'props', toUp, { opts, changed });
toUp.forEach((child) => {
const propsChanged = { ...changed };
// Avoid updating those with override
keys(propsChanged).map((prop) => {
if (isSymbolOverride(child, prop)) delete propsChanged[prop];
});
child.set(propsChanged, { fromInstance: symbol, ...opts });
});
}
const shouldPropagateProperty = (props: Record<string, any>, prop: string, component: Component): boolean => {
const isCollectionVariableDefinition = (() => {
if (prop === 'attributes') {
const attributes = props['attributes'];
return Object.values(attributes).some((attr: any) => attr?.type === DataCollectionVariableType);
}
return props[prop]?.type === DataCollectionVariableType;
})();
return !isSymbolOverride(component, prop) || isCollectionVariableDefinition;
};
export const updateSymbolCls = (symbol: Component, opts: any = {}) => {
@ -193,6 +217,9 @@ export const updateSymbolComps = (symbol: Component, m: Component, c: Components
toUp.forEach((rel) => {
const relCmps = rel.components();
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`
// where we can receive an array of regulat components or already
// existing symbols (updated already before reset)
@ -202,6 +229,7 @@ export const updateSymbolComps = (symbol: Component, m: Component, c: Components
}
return relCmps.at(i);
});
relCmps.reset(toReset, { fromInstance: symbol, ...c } as any);
});
// 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 { AddOptions, Nullable, OptionAsDocument } from '../../common';
import EditorModel from '../../editor/model/Editor';
@ -11,7 +12,7 @@ import Component from './Component';
import Components from './Components';
import { ToolbarButtonProps } from './ToolbarButton';
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' | '';
@ -253,7 +254,7 @@ export interface ComponentProperties {
[key: string]: any;
}
export interface SymbolToUpOptions {
export interface SymbolToUpOptions extends DynamicWatchersOptions {
changed?: string;
fromInstance?: boolean;
noPropagate?: boolean;
@ -321,4 +322,5 @@ export interface ComponentOptions {
frame?: Frame;
temporary?: 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 { Model, ObjectAny, ObjectHash, SetOptions, View } from '../../common';
import { Model, ObjectAny, ObjectHash, SetOptions } from '../../common';
import ParserHtml from '../../parser/model/ParserHtml';
import Selectors from '../../selector_manager/model/Selectors';
import { shallowDiff } from '../../utils/mixins';
import EditorModel from '../../editor/model/Editor';
import StyleDataVariable from '../../data_sources/model/StyleDataVariable';
import { DataVariableDefinition, DataVariableType } from '../../data_sources/model/DataVariable';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
import DataVariable, { DataVariableProps, DataVariableType } from '../../data_sources/model/DataVariable';
import DataResolverListener from '../../data_sources/model/DataResolverListener';
import CssRuleView from '../../css_composer/view/CssRuleView';
import ComponentView from '../../dom_components/view/ComponentView';
import Frame from '../../canvas/model/Frame';
import {
DataCondition,
ConditionalVariableType,
ConditionalVariableDefinition,
DataConditionType,
DataConditionProps,
} from '../../data_sources/model/conditional_variables/DataCondition';
import { isDynamicValue, isDynamicValueDefinition } from '../../data_sources/model/utils';
import { DynamicValueDefinition } from '../../data_sources/types';
export type StyleProps = Record<string, string | string[] | DataVariableDefinition | ConditionalVariableDefinition>;
import { isDataResolver, isDataResolverProps } from '../../data_sources/model/utils';
import { DataResolverProps } from '../../data_sources/types';
export type UpdateStyleOptions = SetOptions & {
export type StyleProps = Record<string, string | string[] | DataVariableProps | DataConditionProps>;
export interface UpdateStyleOptions extends SetOptions {
partial?: boolean;
addStyle?: StyleProps;
inline?: boolean;
noEvent?: boolean;
};
}
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> {
em?: EditorModel;
dynamicVariableListeners: Record<string, DynamicVariableListenerManager> = {};
views: StyleableView[] = [];
styleResolverListeners: Record<string, DataResolverListener> = {};
constructor(attributes: T, options: { em?: EditorModel } = {}) {
super(attributes, options);
@ -71,7 +71,7 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
const result: ObjectAny = { ...style };
if (this.em && !opts.skipResolve) {
const resolvedStyle = this.resolveDataVariables({ ...result });
const resolvedStyle = this.getResolvedStyles({ ...result });
// @ts-ignore
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];
if (isDynamicValueDefinition(styleValue)) {
const styleDynamicVariable = this.resolveDynamicValue(styleValue);
newStyle[key] = styleDynamicVariable;
this.manageDataVariableListener(styleDynamicVariable, key);
if (isDataResolverProps(styleValue)) {
const dataResolver = this.getDataResolverInstance(styleValue);
if (dataResolver) {
newStyle[key] = dataResolver;
this.listenToDataResolver(dataResolver, key);
}
}
});
@ -139,38 +141,33 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
return newStyle;
}
private resolveDynamicValue(styleValue: DynamicValueDefinition) {
const dynamicType = styleValue.type;
let styleDynamicVariable;
switch (dynamicType) {
private getDataResolverInstance(props: DataResolverProps) {
const em = this.em!;
let resolver;
switch (props.type) {
case DataVariableType:
styleDynamicVariable = new StyleDataVariable(styleValue, { em: this.em });
resolver = new DataVariable(props, { em });
break;
case ConditionalVariableType: {
const { condition, ifTrue, ifFalse } = styleValue;
styleDynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: this.em! });
case DataConditionType: {
const { condition, ifTrue, ifFalse } = props;
resolver = new DataCondition(condition, ifTrue, ifFalse, { em });
break;
}
default:
throw new Error(
`Unsupported dynamic value type for styles. Only '${DataVariableType}' and '${ConditionalVariableType}' are supported. Received '${dynamicType}'.`,
);
}
return styleDynamicVariable;
return resolver;
}
/**
* Manage DataVariableListenerManager for a style property
*/
manageDataVariableListener(dataVar: StyleDataVariable | DataCondition, styleProp: string) {
if (this.dynamicVariableListeners[styleProp]) {
this.dynamicVariableListeners[styleProp].listenToDynamicVariable();
listenToDataResolver(resolver: DataVariable | DataCondition, styleProp: string) {
const resolverListener = this.styleResolverListeners[styleProp];
if (resolverListener) {
resolverListener.listenToResolver();
} else {
this.dynamicVariableListeners[styleProp] = new DynamicVariableListenerManager({
this.styleResolverListeners[styleProp] = new DataResolverListener({
em: this.em!,
dataVariable: dataVar,
updateValueFromDataVariable: () => this.updateView(),
resolver,
onUpdate: () => this.updateView(),
});
}
}
@ -195,28 +192,29 @@ export default class StyleableModel<T extends ObjectHash = any> extends Model<T>
this.views.forEach((view) => view.updateStyles());
}
/**
* Resolve dynamic values ( datasource variables - conditional variables ) to their actual values
*/
resolveDataVariables(style: StyleProps): StyleProps {
const resolvedStyle = { ...style };
keys(resolvedStyle).forEach((key) => {
const styleValue = resolvedStyle[key];
getResolvedStyles(style: StyleProps): StyleProps {
const resultStyle = { ...style };
keys(resultStyle).forEach((key) => {
const styleValue = resultStyle[key];
if (typeof styleValue === 'string' || Array.isArray(styleValue)) {
return;
}
if (isDynamicValueDefinition(styleValue)) {
const dataVar = this.resolveDynamicValue(styleValue);
resolvedStyle[key] = dataVar.getDataValue();
if (isDataResolverProps(styleValue)) {
const resolver = this.getDataResolverInstance(styleValue);
if (resolver) {
resultStyle[key] = resolver.getDataValue();
}
}
if (isDynamicValue(styleValue)) {
resolvedStyle[key] = styleValue.getDataValue();
if (isDataResolver(styleValue)) {
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 Category from '../../abstract/ModuleCategory';
import { LocaleOptions, Model, SetOptions } from '../../common';
@ -8,8 +7,6 @@ import { isDef } from '../../utils/mixins';
import TraitsEvents, { TraitGetValueOptions, TraitOption, TraitProperties, TraitSetValueOptions } from '../types';
import TraitView from '../view/TraitView';
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`.
@ -29,8 +26,6 @@ export default class Trait extends Model<TraitProperties> {
em: EditorModel;
view?: TraitView;
el?: HTMLElement;
dynamicVariable?: TraitDataVariable | DataCondition;
dynamicVariableListener?: DynamicVariableListenerManager;
defaults() {
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;
};
export const serialize = (obj: ObjectAny) => JSON.parse(JSON.stringify(obj));
export const isBultInMethod = (key: string) => isFunction(obj[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 { DataVariableType } from '../../../../../src/data_sources/model/DataVariable';
import { MissingConditionError } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { ConditionalVariableType } from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import {
MissingConditionError,
DataConditionType,
} from '../../../../../src/data_sources/model/conditional_variables/DataCondition';
import { GenericOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/GenericOperator';
import { NumberOperation } from '../../../../../src/data_sources/model/conditional_variables/operators/NumberOperator';
import { DataSourceProps } from '../../../../../src/data_sources/types';
import ConditionalComponentView from '../../../../../src/data_sources/view/ComponentDynamicView';
import ComponentDataConditionView from '../../../../../src/data_sources/view/ComponentDataConditionView';
import ComponentWrapper from '../../../../../src/dom_components/model/ComponentWrapper';
import ComponentTableView from '../../../../../src/dom_components/view/ComponentTableView';
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', () => {
const component = cmpRoot.append({
type: ConditionalVariableType,
type: DataConditionType,
condition: {
left: 0,
operator: NumberOperation.greaterThan,
@ -41,10 +42,10 @@ describe('ComponentConditionalVariable', () => {
},
})[0];
expect(component).toBeDefined();
expect(component.get('type')).toBe(ConditionalVariableType);
expect(component.get('type')).toBe(DataConditionType);
expect(component.getInnerHTML()).toBe('<h1>some text</h1>');
const componentView = component.getView();
expect(componentView).toBeInstanceOf(ConditionalComponentView);
expect(componentView).toBeInstanceOf(ComponentDataConditionView);
expect(componentView?.el.textContent).toBe('some text');
const childComponent = getFirstChild(component);
@ -58,7 +59,7 @@ describe('ComponentConditionalVariable', () => {
it('should add a component with a condition that evaluates a string', () => {
const component = cmpRoot.append({
type: ConditionalVariableType,
type: DataConditionType,
condition: {
left: 0,
operator: NumberOperation.greaterThan,
@ -67,10 +68,10 @@ describe('ComponentConditionalVariable', () => {
ifTrue: '<h1>some text</h1>',
})[0];
expect(component).toBeDefined();
expect(component.get('type')).toBe(ConditionalVariableType);
expect(component.get('type')).toBe(DataConditionType);
expect(component.getInnerHTML()).toBe('<h1>some text</h1>');
const componentView = component.getView();
expect(componentView).toBeInstanceOf(ConditionalComponentView);
expect(componentView).toBeInstanceOf(ComponentDataConditionView);
expect(componentView?.el.textContent).toBe('some text');
const childComponent = getFirstChild(component);
@ -93,7 +94,7 @@ describe('ComponentConditionalVariable', () => {
dsm.add(dataSource);
const component = cmpRoot.append({
type: ConditionalVariableType,
type: DataConditionType,
condition: {
left: {
type: DataVariableType,
@ -142,7 +143,7 @@ describe('ComponentConditionalVariable', () => {
dsm.add(dataSource);
const component = cmpRoot.append({
type: ConditionalVariableType,
type: DataConditionType,
condition: {
left: {
type: DataVariableType,
@ -158,7 +159,7 @@ describe('ComponentConditionalVariable', () => {
tagName: 'div',
components: [
{
type: ConditionalVariableType,
type: DataConditionType,
condition: {
left: {
type: DataVariableType,
@ -189,7 +190,7 @@ describe('ComponentConditionalVariable', () => {
it('should store conditional components', () => {
const conditionalCmptDef = {
type: ConditionalVariableType,
type: DataConditionType,
condition: {
left: 0,
operator: NumberOperation.greaterThan,
@ -215,7 +216,7 @@ describe('ComponentConditionalVariable', () => {
it('should throw an error if no condition is passed', () => {
const conditionalCmptDef = {
type: ConditionalVariableType,
type: DataConditionType,
ifTrue: {
tagName: 'h1',
type: 'text',

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

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

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

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