Browse Source

Add collection variables

collection-component
mohamedsalem401 2 years ago
parent
commit
9837116772
  1. 114
      packages/core/src/data_sources/model/collection_component/CollectionComponent.ts
  2. 91
      packages/core/src/data_sources/model/collection_component/CollectionVariable.ts
  3. 4
      packages/core/src/data_sources/model/collection_component/constants.ts
  4. 34
      packages/core/src/data_sources/model/collection_component/types.ts
  5. 33
      packages/core/src/data_sources/model/utils.ts
  6. 9
      packages/core/src/data_sources/types.ts
  7. 30
      packages/core/src/dom_components/model/Component.ts
  8. 10
      packages/core/src/dom_components/model/ComponentDynamicValueWatcher.ts
  9. 30
      packages/core/src/dom_components/model/DynamicValueWatcher.ts
  10. 8
      packages/core/test/specs/dom_components/model/ComponentTypes.ts

114
packages/core/src/data_sources/model/collection_component/CollectionComponent.ts

@ -1,60 +1,24 @@
import { DataVariableDefinition, DataVariableType } from './../DataVariable';
import { DataVariableType } from './../DataVariable';
import { isArray } from 'underscore';
import Component from '../../../dom_components/model/Component';
import { ComponentDefinition, ComponentOptions, ComponentProperties } from '../../../dom_components/model/types';
import { toLowerCase } from '../../../utils/mixins';
import { ConditionDefinition } from '../conditional_variables/DataCondition';
import DataSource from '../DataSource';
import { ObjectAny } from '../../../common';
import EditorModel from '../../../editor/model/Editor';
export const CollectionComponentType = 'collection-component';
export const CollectionVariableType = 'parent-collection-variable';
type CollectionVariable = {
type: typeof CollectionVariableType;
variable_type: keyof CollectionState;
collection_name?: string;
path?: string;
};
type CollectionDataSource = any[] | DataVariableDefinition | CollectionVariable;
type CollectionConfig = {
start_index?: number;
end_index?: number | ConditionDefinition;
dataSource: CollectionDataSource;
};
type CollectionState = {
current_index: number;
start_index: number;
current_item: any;
end_index: number;
collection_name?: string;
total_items: number;
remaining_items: number;
};
type CollectionsStateMap = {
[key: string]: CollectionState;
};
type CollectionDefinition = {
type: typeof CollectionComponentType;
collection_name?: string;
config: CollectionConfig;
block: ComponentDefinition;
};
export const collectionDefinitionKey = 'collectionDefinition';
export const collectionsStateMapKey = 'collectionsItems';
export const innerCollectionStateKey = 'innerCollectionState';
import { keyCollectionsStateMap } from '../../../dom_components/model/Component';
import { CollectionDefinition, CollectionState, CollectionsStateMap } from './types';
import {
keyCollectionDefinition,
keyInnerCollectionState,
CollectionComponentType,
CollectionVariableType,
} from './constants';
export default class CollectionComponent extends Component {
constructor(props: CollectionDefinition & ComponentProperties, opt: ComponentOptions) {
const em = opt.em;
const { collection_name, block, config } = props[collectionDefinitionKey];
const { collection_name, block, config } = props[keyCollectionDefinition];
if (!block) {
throw new Error('The "block" property is required in the collection definition.');
}
@ -83,16 +47,22 @@ export default class CollectionComponent extends Component {
};
const collectionsStateMap: CollectionsStateMap = {
...props[collectionsStateMapKey],
...props[keyCollectionsStateMap],
...(collection_name && { [collection_name]: collectionState }),
[innerCollectionStateKey]: collectionState,
[keyInnerCollectionState]: collectionState,
};
if (index === start_index) {
const { clonedBlock } = resolveBlockValues(collectionsStateMap, block);
const type = em.Components.getType(clonedBlock?.type || 'default');
const model = type.model;
blockComponent = new model(clonedBlock, opt);
blockComponent = new model(
{
...clonedBlock,
[keyCollectionsStateMap]: collectionsStateMap,
},
opt,
);
}
const instance = em.Components.addSymbol(blockComponent!);
const cmpDefinition = resolveComponent(instance!, block, collectionsStateMap, em);
@ -172,12 +142,13 @@ function resolveComponent(
const componentDefinition: ComponentDefinition = {
...componentJSON,
components: children,
[collectionsStateMapKey]: collectionsStateMap,
[keyCollectionsStateMap]: collectionsStateMap,
};
return componentDefinition;
}
// TODO: remove this function
function resolveBlockValues(collectionsStateMap: CollectionsStateMap, block: ObjectAny) {
const clonedBlock = deepCloneObject(block);
const resolvedCollectionValues: ObjectAny = {};
@ -186,26 +157,12 @@ function resolveBlockValues(collectionsStateMap: CollectionsStateMap, block: Obj
const blockKeys = Object.keys(clonedBlock);
for (const key of blockKeys) {
let blockValue = clonedBlock[key];
if (key === collectionDefinitionKey) continue;
if (key === keyCollectionDefinition) continue;
let hasCollectionVariable = false;
if (typeof blockValue === 'object') {
const isCollectionVariable = blockValue.type === CollectionVariableType;
const isCollectionVariable = blockValue.type === 'parent-collection-variable';
if (isCollectionVariable) {
const {
variable_type,
collection_name = innerCollectionStateKey,
path = '',
} = blockValue as CollectionVariable;
const collectionItem = collectionsStateMap[collection_name];
if (!collectionItem) {
throw new Error(`Collection not found: ${collection_name}`);
}
if (!variable_type) {
throw new Error(`Missing collection variable type for collection: ${collection_name}`);
}
clonedBlock[key] = resolveCurrentItem(variable_type, collectionItem, path);
hasCollectionVariable = true;
} else if (Array.isArray(blockValue)) {
clonedBlock[key] = blockValue.map((arrayItem: any) => {
@ -241,31 +198,6 @@ function resolveBlockValues(collectionsStateMap: CollectionsStateMap, block: Obj
return { clonedBlock, resolvedCollectionValues };
}
function resolveCurrentItem(
variableType: CollectionVariable['variable_type'],
collectionItem: CollectionState,
path: string,
) {
if (variableType === 'current_item') {
const valueIsDataVariable = collectionItem.current_item?.type === DataVariableType;
if (valueIsDataVariable) {
const currentItem_path = collectionItem.current_item.path;
const resolvedPath = currentItem_path ? `${currentItem_path}.${path}` : path;
return {
...collectionItem.current_item,
path: resolvedPath,
};
} else if (!!path) {
if (!collectionItem.current_item?.[path]) {
throw new Error(`Path not found in current item: ${path}`);
}
return collectionItem.current_item[path];
}
}
return collectionItem[variableType];
}
function isEmptyObject(itemOverrideKeys: ObjectAny) {
return Object.keys(itemOverrideKeys).length === 0;
}

91
packages/core/src/data_sources/model/collection_component/CollectionVariable.ts

@ -0,0 +1,91 @@
import { CollectionVariableDefinition } from '../../../../test/specs/dom_components/model/ComponentTypes';
import { Model } from '../../../common';
import EditorModel from '../../../editor/model/Editor';
import DataVariable, { DataVariableType } from '../DataVariable';
import { keyInnerCollectionState } from './constants';
import { CollectionsStateMap } from './types';
export default class CollectionVariable extends Model<CollectionVariableDefinition> {
em: EditorModel;
collectionsStateMap: CollectionsStateMap;
dataVariable?: DataVariable;
constructor(
attrs: CollectionVariableDefinition,
options: {
em: EditorModel;
collectionsStateMap: CollectionsStateMap;
},
) {
super(attrs, options);
this.em = options.em;
this.collectionsStateMap = options.collectionsStateMap;
if (!this.collectionsStateMap) {
throw new Error('collectionsStateMap is required');
}
this.updateDataVariable();
}
getDataValue() {
const { resolvedValue } = this.updateDataVariable();
if (resolvedValue?.type === DataVariableType) {
return this.dataVariable!.getDataValue();
}
return resolvedValue;
}
private updateDataVariable() {
const resolvedValue = resolveCollectionVariable(
this.attributes as CollectionVariableDefinition,
this.collectionsStateMap,
);
let dataVariable;
if (resolvedValue?.type === DataVariableType) {
dataVariable = new DataVariable(resolvedValue, { em: this.em });
this.dataVariable = dataVariable;
}
return { resolvedValue, dataVariable };
}
destroy() {
return this.dataVariable?.destroy?.() || super.destroy();
}
}
function resolveCollectionVariable(
collectionVariableDefinition: CollectionVariableDefinition,
collectionsStateMap: CollectionsStateMap,
) {
const { collection_name = keyInnerCollectionState, variable_type, path } = collectionVariableDefinition;
const collectionItem = collectionsStateMap[collection_name];
if (!collectionItem) {
throw new Error(`Collection not found: ${collection_name}`);
}
if (!variable_type) {
throw new Error(`Missing collection variable type for collection: ${collection_name}`);
}
if (variable_type === 'current_item') {
const valueIsDataVariable = collectionItem.current_item?.type === DataVariableType;
if (valueIsDataVariable) {
const currentItem_path = collectionItem.current_item.path;
const resolvedPath = currentItem_path ? `${currentItem_path}.${path}` : path;
return {
...collectionItem.current_item,
path: resolvedPath,
};
} else if (!!path) {
if (!collectionItem.current_item?.[path]) {
throw new Error(`Path not found in current item: ${path} for collection: ${collection_name}`);
}
return collectionItem.current_item[path];
}
}
return collectionItem[variable_type];
}

4
packages/core/src/data_sources/model/collection_component/constants.ts

@ -0,0 +1,4 @@
export const CollectionComponentType = 'collection-component';
export const keyCollectionDefinition = 'collectionDefinition';
export const keyInnerCollectionState = 'innerCollectionState';
export const CollectionVariableType = 'parent-collection-variable';

34
packages/core/src/data_sources/model/collection_component/types.ts

@ -0,0 +1,34 @@
import { CollectionComponentType } from './constants';
import { ComponentDefinition } from '../../../dom_components/model/types';
import { CollectionVariableDefinition } from '../../../../test/specs/dom_components/model/ComponentTypes';
import { DataVariableDefinition } from '../DataVariable';
import { ConditionDefinition } from '../conditional_variables/DataCondition';
type CollectionDataSource = any[] | DataVariableDefinition | CollectionVariableDefinition;
type CollectionConfig = {
start_index?: number;
end_index?: number | ConditionDefinition;
dataSource: CollectionDataSource;
};
export type CollectionState = {
current_index: number;
start_index: number;
current_item: any;
end_index: number;
collection_name?: string;
total_items: number;
remaining_items: number;
};
export type CollectionsStateMap = {
[key: string]: CollectionState;
};
export type CollectionDefinition = {
type: typeof CollectionComponentType;
collection_name?: string;
config: CollectionConfig;
block: ComponentDefinition;
};

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

@ -1,10 +1,17 @@
import EditorModel from '../../editor/model/Editor';
import { DynamicValue, DynamicValueDefinition } from '../types';
import { CollectionsStateMap } from './collection_component/types';
import CollectionVariable from './collection_component/CollectionVariable';
import { CollectionVariableDefinition } from '../../../test/specs/dom_components/model/ComponentTypes';
import { CollectionVariableType } from './collection_component/constants';
import { ConditionalVariableType, DataCondition } from './conditional_variables/DataCondition';
import DataVariable, { DataVariableType } from './DataVariable';
export function isDynamicValueDefinition(value: any): value is DynamicValueDefinition {
return typeof value === 'object' && [DataVariableType, ConditionalVariableType].includes(value?.type);
return (
typeof value === 'object' &&
[DataVariableType, ConditionalVariableType, CollectionVariableType].includes(value?.type)
);
}
export function isDynamicValue(value: any): value is DynamicValue {
@ -23,7 +30,14 @@ export function evaluateVariable(variable: any, em: EditorModel) {
return isDataVariable(variable) ? new DataVariable(variable, { em }).getDataValue() : variable;
}
export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition, em: EditorModel): DynamicValue {
export function getDynamicValueInstance(
valueDefinition: DynamicValueDefinition,
options: {
em: EditorModel;
collectionsStateMap?: CollectionsStateMap;
},
): DynamicValue {
const { em } = options;
const dynamicType = valueDefinition.type;
let dynamicVariable: DynamicValue;
@ -36,6 +50,11 @@ export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition,
dynamicVariable = new DataCondition(condition, ifTrue, ifFalse, { em: em });
break;
}
case CollectionVariableType: {
// @ts-ignore
dynamicVariable = new CollectionVariable(valueDefinition, options);
break;
}
default:
throw new Error(`Unsupported dynamic type: ${dynamicType}`);
}
@ -43,8 +62,14 @@ export function getDynamicValueInstance(valueDefinition: DynamicValueDefinition,
return dynamicVariable;
}
export function evaluateDynamicValueDefinition(valueDefinition: DynamicValueDefinition, em: EditorModel) {
const dynamicVariable = getDynamicValueInstance(valueDefinition, em);
export function evaluateDynamicValueDefinition(
valueDefinition: DynamicValueDefinition,
options: {
em: EditorModel;
collectionsStateMap?: CollectionsStateMap;
},
) {
const dynamicVariable = getDynamicValueInstance(valueDefinition, options);
return { variable: dynamicVariable, value: dynamicVariable.getDataValue() };
}

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

@ -1,12 +1,17 @@
import { ObjectAny } from '../common';
import CollectionVariable from './model/collection_component/CollectionVariable';
import { CollectionVariableDefinition } from '../../test/specs/dom_components/model/ComponentTypes';
import ComponentDataVariable from './model/ComponentDataVariable';
import DataRecord from './model/DataRecord';
import DataRecords from './model/DataRecords';
import DataVariable, { DataVariableDefinition } from './model/DataVariable';
import { ConditionalVariableDefinition, DataCondition } from './model/conditional_variables/DataCondition';
export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition;
export type DynamicValueDefinition = DataVariableDefinition | ConditionalVariableDefinition;
export type DynamicValue = DataVariable | ComponentDataVariable | DataCondition | CollectionVariable;
export type DynamicValueDefinition =
| DataVariableDefinition
| ConditionalVariableDefinition
| CollectionVariableDefinition;
export interface DataRecordProps extends ObjectAny {
/**
* Record id.

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

@ -1,3 +1,4 @@
import { CollectionsStateMap } from '../../data_sources/model/collection_component/types';
import {
isUndefined,
isFunction,
@ -53,7 +54,7 @@ import {
} from './SymbolUtils';
import { ComponentDynamicValueWatcher } from './ComponentDynamicValueWatcher';
import { DynamicValueWatcher } from './DynamicValueWatcher';
import { DynamicValueDefinition } from '../../data_sources/types';
import { CollectionVariableType } from '../../data_sources/model/collection_component/constants';
export interface IComponent extends ExtractMethods<Component> {}
export interface DynamicWatchersOptions {
@ -75,6 +76,7 @@ export const keySymbol = '__symbol';
export const keySymbolOvrd = '__symbol_ovrd';
export const keyUpdate = ComponentsEvents.update;
export const keyUpdateInside = ComponentsEvents.updateInside;
export const keyCollectionsStateMap = '__collections_state_map';
/**
* The Component object represents a single node of our template structure, so when you update its properties the changes are
@ -265,8 +267,22 @@ export default class Component extends StyleableModel<ComponentProperties> {
componentDVListener: ComponentDynamicValueWatcher;
constructor(props: ComponentProperties = {}, opt: ComponentOptions) {
super(props, opt);
this.componentDVListener = new ComponentDynamicValueWatcher(this, opt.em);
if (props[keyCollectionsStateMap]) {
// @ts-ignore
props.components = props.components?.forEach((component) => ({
...component,
[keyCollectionsStateMap]: props[keyCollectionsStateMap],
}));
}
super(props, {
...opt,
// @ts-ignore
[keyCollectionsStateMap]: props[keyCollectionsStateMap],
});
this.componentDVListener = new ComponentDynamicValueWatcher(this, {
em: opt.em,
collectionsStateMap: props[keyCollectionsStateMap],
});
this.componentDVListener.addProps(props);
bindAll(this, '__upSymbProps', '__upSymbCls', '__upSymbComps');
@ -357,7 +373,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
// @ts-ignore
const em = this.em || options.em;
const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attributes, em);
// @ts-ignore
const collectionsStateMap = this.get(keyCollectionsStateMap) || options[keyCollectionsStateMap];
const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attributes, { em, collectionsStateMap });
const shouldSkipWatcherUpdates = options.skipWatcherUpdates || options.fromDataSource;
if (!shouldSkipWatcherUpdates) {
@ -687,7 +705,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
setAttributes(attrs: ObjectAny, opts: SetAttrOptions = { skipWatcherUpdates: false, fromDataSource: false }) {
// @ts-ignore
const em = this.em || opts.em;
const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attrs, em);
// @ts-ignore
const collectionsStateMap = this.get(keyCollectionsStateMap) || opts[keyCollectionsStateMap];
const evaluatedAttributes = DynamicValueWatcher.getStaticValues(attrs, { em, collectionsStateMap });
const shouldSkipWatcherUpdates = opts.skipWatcherUpdates || opts.fromDataSource;
if (!shouldSkipWatcherUpdates) {
this.componentDVListener.setAttributes(attrs);

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

@ -1,4 +1,5 @@
import { ObjectAny } from '../../common';
import { CollectionsStateMap } from '../../data_sources/model/collection_component/types';
import EditorModel from '../../editor/model/Editor';
import Component from './Component';
import { DynamicValueWatcher } from './DynamicValueWatcher';
@ -9,10 +10,13 @@ export class ComponentDynamicValueWatcher {
constructor(
private component: Component,
em: EditorModel,
options: {
em: EditorModel;
collectionsStateMap: CollectionsStateMap;
},
) {
this.propertyWatcher = new DynamicValueWatcher(this.createPropertyUpdater(), em);
this.attributeWatcher = new DynamicValueWatcher(this.createAttributeUpdater(), em);
this.propertyWatcher = new DynamicValueWatcher(this.createPropertyUpdater(), options);
this.attributeWatcher = new DynamicValueWatcher(this.createAttributeUpdater(), options);
}
private createPropertyUpdater() {

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

@ -1,3 +1,4 @@
import { CollectionsStateMap } from '../../data_sources/model/collection_component/types';
import { ObjectAny } from '../../common';
import DynamicVariableListenerManager from '../../data_sources/model/DataVariableListenerManager';
import { evaluateDynamicValueDefinition, isDynamicValueDefinition } from '../../data_sources/model/utils';
@ -5,13 +6,27 @@ import { DynamicValue } from '../../data_sources/types';
import EditorModel from '../../editor/model/Editor';
export class DynamicValueWatcher {
dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {};
private dynamicVariableListeners: { [key: string]: DynamicVariableListenerManager } = {};
private em: EditorModel;
private collectionsStateMap: CollectionsStateMap | undefined;
constructor(
private updateFn: (key: string, value: any) => void,
private em: EditorModel,
) {}
options: {
em: EditorModel;
collectionsStateMap?: CollectionsStateMap;
},
) {
this.em = options.em;
this.collectionsStateMap = options.collectionsStateMap;
}
static getStaticValues(values: ObjectAny | undefined, em: EditorModel): ObjectAny {
static getStaticValues(
values: ObjectAny | undefined,
options: {
em: EditorModel;
collectionsStateMap?: CollectionsStateMap;
},
): ObjectAny {
if (!values) return {};
const evaluatedValues: ObjectAny = { ...values };
const propsKeys = Object.keys(values);
@ -20,7 +35,7 @@ export class DynamicValueWatcher {
const valueDefinition = values[key];
if (!isDynamicValueDefinition(valueDefinition)) continue;
const { value } = evaluateDynamicValueDefinition(valueDefinition, em);
const { value } = evaluateDynamicValueDefinition(valueDefinition, options);
evaluatedValues[key] = value;
}
@ -68,7 +83,10 @@ export class DynamicValueWatcher {
if (!isDynamicValueDefinition(values[key])) {
continue;
}
const { variable } = evaluateDynamicValueDefinition(values[key], this.em);
const { variable } = evaluateDynamicValueDefinition(values[key], {
em: this.em,
collectionsStateMap: this.collectionsStateMap,
});
dynamicValues[key] = variable;
}

8
packages/core/test/specs/dom_components/model/ComponentTypes.ts

@ -1,3 +1,5 @@
import { CollectionVariableType } from '../../../../src/data_sources/model/collection_component/constants';
import { CollectionState } from '../../../../src/data_sources/model/collection_component/types';
import Editor from '../../../../src/editor';
describe('Component Types', () => {
@ -96,3 +98,9 @@ describe('Component Types', () => {
expect(cmp.components().at(0).is('svg-in')).toBe(true);
});
});
export type CollectionVariableDefinition = {
type: typeof CollectionVariableType;
variable_type: keyof CollectionState;
collection_name?: string;
path?: string;
};

Loading…
Cancel
Save