diff --git a/packages/core/src/data_sources/model/ComponentDataVariable.ts b/packages/core/src/data_sources/model/ComponentDataVariable.ts index 31805f0f2..bf44bc4dc 100644 --- a/packages/core/src/data_sources/model/ComponentDataVariable.ts +++ b/packages/core/src/data_sources/model/ComponentDataVariable.ts @@ -40,6 +40,14 @@ export default class ComponentDataVariable extends Component { return this.dataResolver.get('path'); } + getCollectionId() { + return this.dataResolver.get('collectionId'); + } + + getVariableType() { + return this.dataResolver.get('variableType'); + } + getDefaultValue() { return this.dataResolver.get('defaultValue'); } @@ -48,14 +56,22 @@ export default class ComponentDataVariable extends Component { return this.dataResolver.getDataValue(); } - getInnerHTML() { - return this.getDataValue(); + resolvesFromCollection() { + return this.dataResolver.resolvesFromCollection(); } getCollectionsStateMap() { return this.get(keyCollectionsStateMap) ?? {}; } + getDataResolver() { + return this.get('dataResolver'); + } + + getInnerHTML() { + return this.getDataValue(); + } + setPath(newPath: string) { this.dataResolver.set('path', newPath); } @@ -65,7 +81,7 @@ export default class ComponentDataVariable extends Component { } setDataResolver(props: DataVariableProps) { - this.dataResolver.set(props); + this.set('dataResolver', props); } /** @@ -90,9 +106,11 @@ export default class ComponentDataVariable extends Component { this.__changesUp({ m: this }); }).bind(this), ); + this.on('change:dataResolver', () => { this.dataResolver.set(this.get('dataResolver')); }); + this.on(`change:${keyCollectionsStateMap}`, (_: Component, value: DataCollectionStateMap) => { this.dataResolver.updateCollectionsStateMap(value); }); diff --git a/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts index f5e21d52d..ab6cef6c7 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ComponentDataCondition.ts @@ -7,11 +7,14 @@ import { ComponentAddType, } from '../../../dom_components/model/types'; import { toLowerCase } from '../../../utils/mixins'; -import { DataCondition, DataConditionOutputChangedEvent, DataConditionProps, DataConditionType } from './DataCondition'; +import { DataCondition, DataConditionProps, DataConditionType } from './DataCondition'; import { ConditionProps } from './DataConditionEvaluator'; import { StringOperation } from './operators/StringOperator'; import { ObjectAny } from '../../../common'; import { DataConditionIfTrueType, DataConditionIfFalseType } from './constants'; +import { ModelDestroyOptions } from 'backbone'; +import { keyCollectionsStateMap } from '../data_collection/constants'; +import { DataCollectionStateMap } from '../data_collection/types'; export type DataConditionDisplayType = typeof DataConditionIfTrueType | typeof DataConditionIfFalseType; @@ -48,15 +51,20 @@ export default class ComponentDataCondition extends Component { } constructor(props: ComponentDataConditionProps, opt: ComponentOptions) { + const collectionsStateMap = props[keyCollectionsStateMap] as DataCollectionStateMap; // @ts-ignore super(props, opt); const { condition } = props.dataResolver; - this.dataResolver = new DataCondition({ condition }, { em: opt.em }); + this.dataResolver = new DataCondition({ condition }, { em: opt.em, collectionsStateMap }); this.listenToPropsChange(); } + getDataResolver() { + return this.get('dataResolver'); + } + isTrue() { return this.dataResolver.isTrue(); } @@ -77,6 +85,10 @@ export default class ComponentDataCondition extends Component { return this.isTrue() ? this.getIfTrueContent() : this.getIfFalseContent(); } + setDataResolver(props: DataConditionProps) { + return this.set('dataResolver', props); + } + setCondition(newCondition: ConditionProps) { this.dataResolver.setCondition(newCondition); } @@ -89,6 +101,10 @@ export default class ComponentDataCondition extends Component { this.setComponentsAtIndex(1, content); } + getCollectionsStateMap() { + return this.get(keyCollectionsStateMap) ?? {}; + } + getInnerHTML(opts?: ToHTMLOptions): string { return this.getOutputContent()?.getInnerHTML(opts) ?? ''; } @@ -99,9 +115,27 @@ export default class ComponentDataCondition extends Component { } private listenToPropsChange() { + this.listenTo( + this.dataResolver, + 'change', + (() => { + this.__changesUp({ m: this }); + }).bind(this), + ); + this.on('change:dataResolver', () => { this.dataResolver.set(this.get('dataResolver')); }); + + this.on(`change:${keyCollectionsStateMap}`, () => { + this.dataResolver.updateCollectionsStateMap(this.get(keyCollectionsStateMap)); + }); + } + + private removePropsListeners() { + this.stopListening(this.dataResolver); + this.off('change:dataResolver'); + this.off(`change:${keyCollectionsStateMap}`); } toJSON(opts?: ObjectAny): ComponentProperties { @@ -117,6 +151,11 @@ export default class ComponentDataCondition extends Component { }; } + destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { + this.removePropsListeners(); + return super.destroy(options); + } + static isComponent(el: HTMLElement) { return toLowerCase(el.tagName) === DataConditionType; } diff --git a/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts index 812275fa4..49dff319c 100644 --- a/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts +++ b/packages/core/src/data_sources/model/conditional_variables/ConditionStatement.ts @@ -1,10 +1,10 @@ -import { Operator } from './operators/BaseOperator'; -import { DataConditionOperation } from './operators/types'; +import { SimpleOperator } from './operators/BaseOperator'; +import { DataConditionSimpleOperation } from './types'; export class ConditionStatement { constructor( private leftValue: any, - private operator: Operator, + private operator: SimpleOperator, private rightValue: any, ) {} diff --git a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts index 765b27d9d..db933fd81 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataCondition.ts @@ -4,12 +4,11 @@ import DataVariable, { DataVariableProps } from '../DataVariable'; import DataResolverListener from '../DataResolverListener'; import { resolveDynamicValue, isDataVariable } from '../../utils'; import { DataConditionEvaluator, ConditionProps } from './DataConditionEvaluator'; -import { AnyTypeOperation } from './operators/AnyTypeOperator'; import { BooleanOperation } from './operators/BooleanOperator'; -import { NumberOperation } from './operators/NumberOperator'; import { StringOperation } from './operators/StringOperator'; import { isUndefined } from 'underscore'; import { DataCollectionStateMap } from '../data_collection/types'; +import { DataConditionSimpleOperation } from './types'; export const DataConditionType = 'data-condition' as const; export const DataConditionEvaluationChangedEvent = 'data-condition-evaluation-changed'; @@ -17,7 +16,7 @@ export const DataConditionOutputChangedEvent = 'data-condition-output-changed'; export interface ExpressionProps { left?: any; - operator?: AnyTypeOperation | StringOperation | NumberOperation; + operator?: DataConditionSimpleOperation; right?: any; } @@ -33,6 +32,12 @@ export interface DataConditionProps { ifFalse?: any; } +type DataConditionOptions = { + em: EditorModel; + onValueChange?: () => void; + collectionsStateMap?: DataCollectionStateMap; +}; + export class DataCondition extends Model { private em: EditorModel; private collectionsStateMap: DataCollectionStateMap = {}; @@ -53,13 +58,14 @@ export class DataCondition extends Model { }; } - constructor(props: DataConditionProps, opts: { em: EditorModel; onValueChange?: () => void }) { + constructor(props: DataConditionProps, opts: DataConditionOptions) { if (isUndefined(props.condition)) { opts.em.logError('No condition was provided to a conditional component.'); } super(props, opts); this.em = opts.em; + this.collectionsStateMap = opts.collectionsStateMap ?? {}; const { condition = {} } = props; const instance = new DataConditionEvaluator({ condition }, { em: this.em }); @@ -80,7 +86,12 @@ export class DataCondition extends Model { return this.get('ifFalse'); } + getOperations() { + return this._conditionEvaluator.getOperations(); + } + setCondition(condition: ConditionProps) { + this.set('condition', condition); this._conditionEvaluator.set('condition', condition); this.trigger(DataConditionOutputChangedEvent, this.getDataValue()); } @@ -115,6 +126,7 @@ export class DataCondition extends Model { updateCollectionsStateMap(collectionsStateMap: DataCollectionStateMap) { this.collectionsStateMap = collectionsStateMap; + this.emitConditionEvaluationChange(); } private listenToPropsChange() { diff --git a/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts b/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts index a51b666a7..a0d00234a 100644 --- a/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/DataConditionEvaluator.ts @@ -1,15 +1,17 @@ import { DataVariableProps } from '../DataVariable'; import EditorModel from '../../../editor/model/Editor'; -import { resolveDynamicValue, isDataVariable } from '../../utils'; +import { resolveDynamicValue, isDataVariable, getDataResolverInstanceValue } from '../../utils'; import { ExpressionProps, LogicGroupProps } from './DataCondition'; import { LogicalGroupEvaluator } from './LogicalGroupEvaluator'; -import { Operator } from './operators/BaseOperator'; +import { SimpleOperator } from './operators/BaseOperator'; import { AnyTypeOperation, AnyTypeOperator } from './operators/AnyTypeOperator'; import { BooleanOperator } from './operators/BooleanOperator'; import { NumberOperator, NumberOperation } from './operators/NumberOperator'; import { StringOperator, StringOperation } from './operators/StringOperator'; import { Model } from '../../../common'; -import { DataConditionOperation } from './operators/types'; +import { DataConditionSimpleOperation } from './types'; +import { isBoolean } from 'underscore'; +import { DataCollectionStateMap } from '../data_collection/types'; export type ConditionProps = ExpressionProps | LogicGroupProps | boolean; @@ -17,45 +19,78 @@ interface DataConditionEvaluatorProps { condition: ConditionProps; } +interface DataConditionEvaluatorOptions { + em: EditorModel; + collectionsStateMap?: DataCollectionStateMap; +} + export class DataConditionEvaluator extends Model { private em: EditorModel; + private collectionsStateMap: DataCollectionStateMap = {}; - constructor(props: DataConditionEvaluatorProps, opts: { em: EditorModel }) { + constructor(props: DataConditionEvaluatorProps, opts: DataConditionEvaluatorOptions) { super(props); this.em = opts.em; + this.collectionsStateMap = opts.collectionsStateMap ?? {}; } evaluate(): boolean { + const condition = this.get('condition'); + if (!condition || isBoolean(condition)) return !!condition; + + const resolvedOperator = this.getOperator(); + if (!resolvedOperator) return false; + + return resolvedOperator.evaluate(this.getResolvedLeftValue(), this.getResolvedRightValue()); + } + + getDependentDataVariables(): DataVariableProps[] { + const condition = this.get('condition'); + if (!condition) return []; + + return this.extractDataVariables(condition); + } + + getOperations() { + const operator = this.getOperator(); + if (!operator || operator instanceof LogicalGroupEvaluator) return []; + + return operator.getOperations(); + } + + updateCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { + this.collectionsStateMap = collectionsStateMap; + } + + private getOperator() { const em = this.em; const condition = this.get('condition'); - if (typeof condition === 'boolean') return condition; + if (!condition || isBoolean(condition)) return; + let resolvedOperator: SimpleOperator | LogicalGroupEvaluator | undefined; if (this.isLogicGroup(condition)) { const { logicalOperator, statements } = condition; const operator = new BooleanOperator(logicalOperator, { em }); - const logicalGroup = new LogicalGroupEvaluator(operator, statements, { em }); - return logicalGroup.evaluate(); + resolvedOperator = new LogicalGroupEvaluator(operator, statements, { em }); } if (this.isExpression(condition)) { - const { left, operator, right } = condition; - const evaluateLeft = resolveDynamicValue(left, this.em); - const evaluateRight = resolveDynamicValue(right, this.em); - const op = this.getOperator(evaluateLeft, operator); - if (!op) return false; - - const evaluated = op.evaluate(evaluateLeft, evaluateRight); - return evaluated; + const { left, operator } = condition; + const evaluatedLeft = resolveDynamicValue(left, em); + + resolvedOperator = this.resolveOperator(evaluatedLeft, operator); } - this.em.logError('Invalid condition type.'); - return false; + return resolvedOperator; } /** * Factory method for creating operators based on the data type. */ - private getOperator(left: any, operator: string | undefined): Operator | undefined { + private resolveOperator( + left: any, + operator: string | undefined, + ): SimpleOperator | undefined { const em = this.em; if (this.isOperatorInEnum(operator, AnyTypeOperation)) { @@ -66,17 +101,9 @@ export class DataConditionEvaluator extends Model { return new StringOperator(operator as StringOperation, { em }); } - this.em?.logError(`Unsupported data type: ${typeof left}`); return; } - getDependentDataVariables(): DataVariableProps[] { - const condition = this.get('condition'); - if (!condition) return []; - - return this.extractDataVariables(condition); - } - private extractDataVariables(condition: ConditionProps): DataVariableProps[] { const variables: DataVariableProps[] = []; @@ -102,6 +129,31 @@ export class DataConditionEvaluator extends Model { return Object.values(enumObject).includes(operator); } + private resolveExpressionSide(property: 'left' | 'right'): any { + const condition = this.get('condition'); + const { em, collectionsStateMap } = this; + + if (!condition || typeof condition === 'boolean') { + return condition; + } + + if (condition && typeof condition === 'object' && property in condition) { + const value = (condition as ExpressionProps)[property]; + + return getDataResolverInstanceValue(value, { em, collectionsStateMap }); + } + + return undefined; + } + + private getResolvedLeftValue(): any { + return this.resolveExpressionSide('left'); + } + + private getResolvedRightValue(): any { + return this.resolveExpressionSide('right'); + } + toJSON(options?: any) { const condition = this.get('condition'); if (typeof condition === 'object') { diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts index 58eacaea3..0f7e07734 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/AnyTypeOperator.ts @@ -1,5 +1,6 @@ +import { enumToArray } from '../../../utils'; import DataVariable from '../../DataVariable'; -import { Operator } from './BaseOperator'; +import { SimpleOperator } from './BaseOperator'; export enum AnyTypeOperation { equals = 'equals', @@ -16,9 +17,11 @@ export enum AnyTypeOperation { isDefaultValue = 'isDefaultValue', // For Datasource variables } -export class AnyTypeOperator extends Operator { +export class AnyTypeOperator extends SimpleOperator { + protected operationsEnum = AnyTypeOperation; + evaluate(left: any, right: any): boolean { - switch (this.operation) { + switch (this.operationString) { case 'equals': return left === right; case 'isTruthy': @@ -44,7 +47,7 @@ export class AnyTypeOperator extends Operator { case 'isDefaultValue': return left instanceof DataVariable && left.get('defaultValue') === right; default: - this.em?.logError(`Unsupported generic operation: ${this.operation}`); + this.em?.logWarning(`Unsupported generic operation: ${this.operationString}`); return false; } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts index 4cde7626d..68ae9660c 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/BaseOperator.ts @@ -1,14 +1,20 @@ import EditorModel from '../../../../editor/model/Editor'; -import { DataConditionOperation } from './types'; +import { enumToArray } from '../../../utils'; +import { DataConditionSimpleOperation } from '../types'; -export abstract class Operator { +export abstract class SimpleOperator { protected em: EditorModel; - protected operation: OperationType; + protected operationString: OperationType; + protected abstract operationsEnum: Record; - constructor(operation: any, opts: { em: EditorModel }) { - this.operation = operation; + constructor(operationString: any, opts: { em: EditorModel }) { + this.operationString = operationString; this.em = opts.em; } abstract evaluate(left: any, right: any): boolean; + + getOperations(): DataConditionSimpleOperation[] { + return enumToArray(this.operationsEnum); + } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts index e06b95a7e..413495694 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/BooleanOperator.ts @@ -1,4 +1,6 @@ -import { Operator } from './BaseOperator'; +import { enumToArray } from '../../../utils'; +import { DataConditionSimpleOperation } from '../types'; +import { SimpleOperator } from './BaseOperator'; export enum BooleanOperation { and = 'and', @@ -6,11 +8,13 @@ export enum BooleanOperation { xor = 'xor', } -export class BooleanOperator extends Operator { +export class BooleanOperator extends SimpleOperator { + protected operationsEnum = BooleanOperation; + evaluate(statements: boolean[]): boolean { if (!statements?.length) return false; - switch (this.operation) { + switch (this.operationString) { case BooleanOperation.and: return statements.every(Boolean); case BooleanOperation.or: @@ -18,7 +22,7 @@ export class BooleanOperator extends Operator { case BooleanOperation.xor: return statements.filter(Boolean).length === 1; default: - this.em.logError(`Unsupported boolean operation: ${this.operation}`); + this.em.logWarning(`Unsupported boolean operation: ${this.operationString}`); return false; } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts index 52fc256af..04f4da498 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/NumberOperator.ts @@ -1,4 +1,5 @@ -import { Operator } from './BaseOperator'; +import { enumToArray } from '../../../utils'; +import { SimpleOperator } from './BaseOperator'; export enum NumberOperation { greaterThan = '>', @@ -9,9 +10,13 @@ export enum NumberOperation { notEquals = '!=', } -export class NumberOperator extends Operator { +export class NumberOperator extends SimpleOperator { + protected operationsEnum = NumberOperation; + evaluate(left: number, right: number): boolean { - switch (this.operation) { + if (typeof left !== 'number') return false; + + switch (this.operationString) { case NumberOperation.greaterThan: return left > right; case NumberOperation.lessThan: @@ -25,7 +30,7 @@ export class NumberOperator extends Operator { case NumberOperation.notEquals: return left !== right; default: - this.em.logError(`Unsupported number operation: ${this.operation}`); + this.em.logWarning(`Unsupported number operation: ${this.operationString}`); return false; } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts index 7dead9629..eb5279867 100644 --- a/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts +++ b/packages/core/src/data_sources/model/conditional_variables/operators/StringOperator.ts @@ -1,4 +1,4 @@ -import { Operator } from './BaseOperator'; +import { SimpleOperator } from './BaseOperator'; export enum StringOperation { contains = 'contains', @@ -9,9 +9,13 @@ export enum StringOperation { trimEquals = 'trimEquals', } -export class StringOperator extends Operator { +export class StringOperator extends SimpleOperator { + protected operationsEnum = StringOperation; + evaluate(left: string, right: string) { - switch (this.operation) { + if (typeof left !== 'string') return false; + + switch (this.operationString) { case StringOperation.contains: return left.includes(right); case StringOperation.startsWith: @@ -19,14 +23,14 @@ export class StringOperator extends Operator { case StringOperation.endsWith: return left.endsWith(right); case StringOperation.matchesRegex: - if (!right) this.em.logError('Regex pattern must be provided.'); - return new RegExp(right).test(left); + if (!right) this.em.logWarning('Regex pattern must be provided.'); + return new RegExp(right ?? '').test(left); case StringOperation.equalsIgnoreCase: return left.toLowerCase() === right.toLowerCase(); case StringOperation.trimEquals: return left.trim() === right.trim(); default: - this.em.logError(`Unsupported string operation: ${this.operation}`); + this.em.logWarning(`Unsupported string operation: ${this.operationString}`); return false; } } diff --git a/packages/core/src/data_sources/model/conditional_variables/operators/types.ts b/packages/core/src/data_sources/model/conditional_variables/operators/types.ts deleted file mode 100644 index 483ca91bc..000000000 --- a/packages/core/src/data_sources/model/conditional_variables/operators/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AnyTypeOperation } from './AnyTypeOperator'; -import { BooleanOperation } from './BooleanOperator'; -import { NumberOperation } from './NumberOperator'; -import { StringOperation } from './StringOperator'; - -export type DataConditionOperation = AnyTypeOperation | StringOperation | NumberOperation | BooleanOperation; diff --git a/packages/core/src/data_sources/model/conditional_variables/types.ts b/packages/core/src/data_sources/model/conditional_variables/types.ts new file mode 100644 index 000000000..346a76c67 --- /dev/null +++ b/packages/core/src/data_sources/model/conditional_variables/types.ts @@ -0,0 +1,7 @@ +import { AnyTypeOperation } from './operators/AnyTypeOperator'; +import { BooleanOperation } from './operators/BooleanOperator'; +import { NumberOperation } from './operators/NumberOperator'; +import { StringOperation } from './operators/StringOperator'; + +export type DataConditionSimpleOperation = AnyTypeOperation | StringOperation | NumberOperation | BooleanOperation; +export type DataConditionCompositeOperation = DataConditionSimpleOperation; diff --git a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts index 6a893768d..8049ad313 100644 --- a/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts +++ b/packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts @@ -1,4 +1,4 @@ -import { bindAll, isArray } from 'underscore'; +import { isArray } from 'underscore'; import { ObjectAny } from '../../../common'; import Component, { keySymbol } from '../../../dom_components/model/Component'; import { ComponentAddType, ComponentDefinitionDefined, ComponentOptions } from '../../../dom_components/model/types'; @@ -25,6 +25,7 @@ import { import { getSymbolsToUpdate } from '../../../dom_components/model/SymbolUtils'; import { StyleProps, UpdateStyleOptions } from '../../../domain_abstract/model/StyleableModel'; import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers'; +import { ModelDestroyOptions } from 'backbone'; const AvoidStoreOptions = { avoidStore: true, partial: true }; export default class ComponentDataCollection extends Component { @@ -59,14 +60,17 @@ export default class ComponentDataCollection extends Component { return cmp; } - bindAll(this, 'rebuildChildrenFromCollection'); - this.listenTo(this, `change:${keyCollectionDefinition}`, this.rebuildChildrenFromCollection); + this.rebuildChildrenFromCollection = this.rebuildChildrenFromCollection.bind(this); + this.listenToPropsChange(); this.rebuildChildrenFromCollection(); - this.listenToDataSource(); return cmp; } + getDataResolver() { + return this.get('dataResolver'); + } + getItemsCount() { const items = this.getDataSourceItems(); const startIndex = Math.max(0, this.getConfigStartIndex() ?? 0); @@ -97,6 +101,10 @@ export default class ComponentDataCollection extends Component { return this.firstChild.components(); } + setDataResolver(props: DataCollectionProps) { + return this.set('dataResolver', props); + } + setCollectionId(collectionId: string) { this.updateCollectionConfig({ collectionId }); } @@ -114,13 +122,6 @@ export default class ComponentDataCollection extends Component { this.updateCollectionConfig({ endIndex }); } - private updateCollectionConfig(updates: Partial): void { - this.set(keyCollectionDefinition, { - ...this.dataResolver, - ...updates, - }); - } - setDataSource(dataSource: DataCollectionDataSource) { this.set(keyCollectionDefinition, { ...this.dataResolver, @@ -136,6 +137,13 @@ export default class ComponentDataCollection extends Component { return this.components().at(0); } + private updateCollectionConfig(updates: Partial): void { + this.set(keyCollectionDefinition, { + ...this.dataResolver, + ...updates, + }); + } + private getDataSourceItems() { return getDataSourceItems(this.dataResolver.dataSource, this.em); } @@ -246,6 +254,19 @@ export default class ComponentDataCollection extends Component { ); } + private listenToPropsChange() { + this.on(`change:${keyCollectionDefinition}`, () => { + this.rebuildChildrenFromCollection(); + this.listenToDataSource(); + }); + this.listenToDataSource(); + } + + private removePropsListeners() { + this.off(`change:${keyCollectionDefinition}`); + this.dataSourceWatcher?.destroy(); + } + static isComponent(el: HTMLElement) { return toLowerCase(el.tagName) === DataCollectionType; } @@ -259,6 +280,11 @@ export default class ComponentDataCollection extends Component { const firstChild = this.firstChild as any; return { ...json, components: [firstChild] }; } + + destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { + this.removePropsListeners(); + return super.destroy(options); + } } function applyToComponentAndChildren(operation: (cmp: Component) => void, component: Component) { @@ -284,36 +310,57 @@ function setCollectionStateMapAndPropagate( const listenerKey = `_hasAddListener${collectionId ? `_${collectionId}` : ''}`; const cmps = cmp.components(); - if (!cmp.collectionStateListeners.includes(listenerKey)) { - cmp.listenTo(cmps, 'add', addListener); - cmp.collectionStateListeners.push(listenerKey); + const removeListener = (component: Component) => { + const index = component.collectionStateListeners?.indexOf(listenerKey) ?? -1; + if (index !== -1) { + component.collectionStateListeners?.splice(index, 1); + } - const removeListener = (component: Component) => { - component.stopListening(component.components(), 'add', addListener); + if (!component.collectionStateListeners?.length) { component.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); - const index = component.collectionStateListeners.indexOf(listenerKey); - if (index !== -1) { - component.collectionStateListeners.splice(index, 1); - } + component.stopListening(component.components(), 'add', addListener); + component.stopListening(component.components(), 'remove', removeListener); + } - const collectionsStateMap = component.get(keyCollectionsStateMap); + const currentCollectionsStateMap = component.get(keyCollectionsStateMap); + if (currentCollectionsStateMap) { component.set(keyCollectionsStateMap, { - ...collectionsStateMap, + ...currentCollectionsStateMap, [collectionId]: undefined, }); - }; + } + }; - cmp.listenTo(cmps, 'remove', removeListener); + const removeAllListeners = (cmp: Component) => { + cmp.components().forEach((child) => removeAllListeners(child)); + cmp.off(`change:${keyCollectionsStateMap}`); + cmp.stopListening(cmp.components(), 'add'); + cmp.stopListening(cmp.components(), 'remove'); + cmp.collectionStateListeners = []; + }; + + if (!cmp.collectionStateListeners) { + cmp.collectionStateListeners = []; + } + + if (!cmp.collectionStateListeners.length) { + cmp.on(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); } - cmps.forEach((cmp) => setCollectionStateMapAndPropagate(cmp, collectionsStateMap, collectionId)); + if (!cmp.collectionStateListeners.includes(listenerKey)) { + cmp.collectionStateListeners.push(listenerKey); + cmp.listenTo(cmps, 'add', addListener); + cmp.listenTo(cmps, 'remove', removeListener); + } - cmp.on(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange); + cmp.__onDestroy = () => removeAllListeners(cmp); + cmps.forEach((childCmp) => setCollectionStateMapAndPropagate(childCmp, collectionsStateMap, collectionId)); }, cmp); } function handleCollectionStateMapChange(this: Component) { const updatedCollectionsStateMap = this.get(keyCollectionsStateMap); + this.components() ?.toArray() .forEach((component: Component) => { @@ -357,7 +404,7 @@ function setCollectionStateMap(collectionsStateMap: DataCollectionStateMap) { ...cmp.get(keyCollectionsStateMap), ...collectionsStateMap, }; - cmp.set(keyCollectionsStateMap, updatedCollectionStateMap); + cmp.set(keyCollectionsStateMap, updatedCollectionStateMap, AvoidStoreOptions); cmp.dataResolverWatchers.updateCollectionStateMap(updatedCollectionStateMap); const parentCollectionsId = Object.keys(updatedCollectionStateMap); diff --git a/packages/core/src/data_sources/utils.ts b/packages/core/src/data_sources/utils.ts index 101a041da..101a52431 100644 --- a/packages/core/src/data_sources/utils.ts +++ b/packages/core/src/data_sources/utils.ts @@ -47,7 +47,7 @@ export function getDataResolverInstance( break; } default: - options.em?.logError(`Unsupported dynamic type: ${type}`); + options.em?.logWarning(`Unsupported dynamic type: ${type}`); return; } @@ -83,3 +83,9 @@ export const ensureComponentInstance = ( export const isComponentDataOutputType = (type: string | undefined) => { return !!type && [DataCollectionItemType, DataConditionIfTrueType, DataConditionIfFalseType].includes(type); }; + +export function enumToArray(enumObj: any) { + return Object.keys(enumObj) + .filter((key) => isNaN(Number(key))) + .map((key) => enumObj[key]); +} diff --git a/packages/core/src/dom_components/model/Component.ts b/packages/core/src/dom_components/model/Component.ts index c9f0cbf26..69ddb05e0 100644 --- a/packages/core/src/dom_components/model/Component.ts +++ b/packages/core/src/dom_components/model/Component.ts @@ -450,6 +450,8 @@ export default class Component extends StyleableModel { updateSymbolComps(this, m, c, o); } + __onDestroy() {} + /** * Check component's type * @param {string} type Component type @@ -1832,6 +1834,7 @@ export default class Component extends StyleableModel { destroy(options?: ModelDestroyOptions | undefined): false | JQueryXHR { this.dataResolverWatchers.destroy(); + this.__onDestroy(); return super.destroy(options); }