Browse Source

Update collections ( add setters and getter, first item editable, sync styles )

pull/6419/head
mohamedsalem401 2 years ago
parent
commit
af5a0c5d1d
  1. 286
      packages/core/src/data_sources/model/data_collection/ComponentDataCollection.ts
  2. 2
      packages/core/src/dom_components/model/ComponentDataResolverWatchers.ts
  3. 339
      packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts

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

@ -17,7 +17,8 @@ import {
DataCollectionState,
DataCollectionStateMap,
} from './types';
import { updateFromWatcher } from '../../../dom_components/model/ComponentDataResolverWatchers';
import { getSymbolsToUpdate } from '../../../dom_components/model/SymbolUtils';
import { StyleProps, UpdateStyleOptions } from '../../../domain_abstract/model/StyleableModel';
export default class ComponentDataCollection extends Component {
constructor(props: ComponentDataCollectionProps, opt: ComponentOptions) {
@ -36,129 +37,199 @@ export default class ComponentDataCollection extends Component {
return cmp;
}
const parentCollectionStateMap = (props[keyCollectionsStateMap] || {}) as DataCollectionStateMap;
const components: Component[] = getCollectionItems(em, collectionDef, parentCollectionStateMap, opt);
cmp.components(components, opt);
return cmp;
}
getItemsCount(): number {
const items = this.getDataSourceItems();
const startIndex = Math.max(0, this.collectionConfig.startIndex || 0);
const configEndIndex = this.getConfigStartIndex() || Number.MAX_VALUE;
const endIndex = Math.min(items.length - 1, configEndIndex);
return endIndex - startIndex + 1;
}
getConfigStartIndex() {
return this.collectionConfig.startIndex;
}
getConfigEndIndex() {
return this.collectionConfig.endIndex;
}
getComponentDef(): ComponentDefinition {
const componentDef = this.getFirstChildJSON();
return componentDef;
}
getDataSource(): DataCollectionDataSource {
const collectionDef = this.collectionDef;
return collectionDef?.collectionConfig?.dataSource;
}
getCollectionId(): string {
const collectionDef = this.collectionDef;
return collectionDef?.collectionConfig?.collectionId;
}
setComponentDef(componentDef: ComponentDefinition) {
const collectionDef = { ...this.collectionDef };
collectionDef.componentDef = componentDef;
this.set(keyCollectionDefinition, collectionDef);
}
if (isDataVariable(this.collectionDataSource)) {
this.watchDataSource(parentCollectionStateMap, opt);
setStartIndex(startIndex: number) {
if (startIndex < 0) {
this.em.logError('Start index should be greater than or equal to 0');
return;
}
return cmp;
const newCollectionConfig = { ...this.collectionConfig };
newCollectionConfig.startIndex = startIndex;
const collectionDef = { ...this.collectionDef, collectionConfig: newCollectionConfig };
this.set(keyCollectionDefinition, collectionDef);
}
get collectionConfig() {
return this.get(keyCollectionDefinition).collectionConfig as DataCollectionConfig;
setEndIndex(endIndex: number) {
const collectionConfig = { ...this.collectionConfig };
collectionConfig.endIndex = endIndex;
const collectionDef = { ...this.collectionDef, collectionConfig };
this.set(keyCollectionDefinition, collectionDef);
}
get collectionDataSource() {
return this.collectionConfig.dataSource;
setDataSource(dataSource: DataCollectionDataSource) {
const collectionConfig = { ...this.collectionConfig };
collectionConfig.dataSource = dataSource;
const collectionDef = { ...this.collectionDef, collectionConfig };
this.set(keyCollectionDefinition, collectionDef);
}
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 getDataSourceItems() {
const collectionDef = this.collectionDef;
const collectionConfig = collectionDef?.collectionConfig;
if (!collectionConfig) {
return [];
}
const items = getDataSourceItems(collectionConfig.dataSource, this.em);
return items;
}
private getComponentDef() {
const firstChild = this.components().at(0);
const firstChildJSON = firstChild ? serialize(firstChild) : this.get(keyCollectionDefinition).componentDef;
delete firstChildJSON?.draggable;
return firstChildJSON;
private getCollectionStateMap() {
return (this.get(keyCollectionsStateMap) || {}) as DataCollectionStateMap;
}
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);
},
});
private get collectionDef() {
return (this.get(keyCollectionDefinition) || {}) as DataCollectionProps;
}
static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataCollectionType;
private get collectionConfig() {
return (this.collectionDef?.collectionConfig || {}) as DataCollectionConfig;
}
}
function getCollectionItems(
em: EditorModel,
collectionDef: DataCollectionProps,
parentCollectionStateMap: DataCollectionStateMap,
opt: ComponentOptions,
) {
const { componentDef, collectionConfig } = collectionDef;
const result = validateCollectionConfig(collectionConfig, componentDef, em);
if (!result) {
return [];
private get collectionDataSource() {
return this.collectionConfig.dataSource;
}
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),
};
private getFirstChildJSON() {
const firstChild = this.components().at(0);
const firstChildJSON = firstChild ? serialize(firstChild) : this.collectionDef.componentDef;
delete firstChildJSON?.draggable;
delete firstChildJSON?.removable;
return firstChildJSON;
}
if (parentCollectionStateMap[collectionId]) {
em.logError(
`The collection ID "${collectionId}" already exists in the parent collection state. Overriding it is not allowed.`,
);
getCollectionItems() {
const { componentDef, collectionConfig } = this.collectionDef;
const result = validateCollectionConfig(collectionConfig, componentDef, this.em);
if (!result) {
return [];
}
const collectionsStateMap: DataCollectionStateMap = {
...parentCollectionStateMap,
[collectionId]: collectionState,
};
const components: Component[] = [];
const collectionId = collectionConfig.collectionId;
const items = this.getDataSourceItems();
const startIndex = this.getConfigStartIndex() ?? 0;
const configEndIndex = this.getConfigEndIndex() ?? Number.MAX_VALUE;
const endIndex = Math.min(items.length - 1, configEndIndex);
const totalItems = endIndex - startIndex + 1;
const parentCollectionStateMap = this.getCollectionStateMap();
let symbolMain: Component;
for (let index = startIndex; index <= endIndex; index++) {
const item = items[index];
const isFirstItem = index === startIndex;
const collectionState: DataCollectionState = {
collectionId,
currentIndex: index,
currentItem: item,
startIndex: startIndex,
endIndex: endIndex,
totalItems: totalItems,
remainingItems: totalItems - (index + 1),
};
if (parentCollectionStateMap[collectionId]) {
this.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 (isFirstItem) {
const componentType = (componentDef?.type as string) || 'default';
let type = this.em.Components.getType(componentType) || this.em.Components.getType('default');
const Model = type.model;
symbolMain = new Model(
{
...serialize(componentDef),
draggable: false,
removable: false,
},
this.opt,
);
}
const instance = symbolMain!.clone({ symbol: true });
!isFirstItem && instance.set('locked', true);
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance);
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);
components.push(instance);
}
const instance = symbolMain!.clone({ symbol: true });
setCollectionStateMapAndPropagate(collectionsStateMap, collectionId)(instance);
return components;
}
components.push(instance);
static isComponent(el: HTMLElement) {
return toLowerCase(el.tagName) === DataCollectionType;
}
return components;
toJSON(opts?: ObjectAny) {
const json = super.toJSON.call(this, opts) as ComponentDataCollectionProps;
json[keyCollectionDefinition].componentDef = this.getFirstChildJSON();
delete json.components;
delete json.droppable;
return json;
}
}
function setCollectionStateMapAndPropagate(
collectionsStateMap: DataCollectionStateMap,
collectionId: string | undefined,
) {
function setCollectionStateMapAndPropagate(collectionsStateMap: DataCollectionStateMap, collectionId: string) {
return (cmp: Component) => {
setCollectionStateMap(collectionsStateMap)(cmp);
@ -178,9 +249,15 @@ function setCollectionStateMapAndPropagate(
component.stopListening(component.components(), 'add', addListener);
component.off(`change:${keyCollectionsStateMap}`, handleCollectionStateMapChange);
const index = component.collectionStateListeners.indexOf(listenerKey);
if (index > -1) {
if (index !== -1) {
component.collectionStateListeners.splice(index, 1);
}
const collectionsStateMap = component.get(keyCollectionsStateMap);
component.set(keyCollectionsStateMap, {
...collectionsStateMap,
[collectionId]: undefined,
});
};
cmp.listenTo(cmps, 'remove', removeListener);
@ -229,6 +306,12 @@ function validateCollectionConfig(
}
}
const startIndex = collectionConfig?.startIndex;
if (startIndex !== undefined && (startIndex < 0 || !Number.isInteger(startIndex))) {
em.logError(`Invalid startIndex: ${startIndex}. It must be a non-negative integer.`);
}
return true;
}
@ -241,6 +324,21 @@ function setCollectionStateMap(collectionsStateMap: DataCollectionStateMap) {
};
cmp.set(keyCollectionsStateMap, updatedCollectionStateMap);
cmp.dataResolverWatchers.updateCollectionStateMap(updatedCollectionStateMap);
const parentCollectionsId = Object.keys(updatedCollectionStateMap);
const isFirstItem = parentCollectionsId.every(
(key) => updatedCollectionStateMap[key].currentIndex === updatedCollectionStateMap[key].startIndex,
);
if (isFirstItem) {
cmp.__onStyleChange = (newStyles: StyleProps, opts: UpdateStyleOptions = {}) => {
const cmps = getSymbolsToUpdate(cmp);
cmps.forEach((cmp) => {
cmp.addStyle(newStyles, opts);
});
};
}
};
}

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

@ -82,7 +82,7 @@ export class ComponentDataResolverWatchers {
const keys = this.propertyWatcher.getDynamicValuesOfType(DataCollectionVariableType);
const attributesKeys = this.attributeWatcher.getDynamicValuesOfType(DataCollectionVariableType);
const combinedKeys = [keyCollectionsStateMap, ...keys];
const combinedKeys = [keyCollectionsStateMap, 'locked', ...keys];
const haveOverridenAttributes = Object.keys(attributesKeys).length;
if (haveOverridenAttributes) combinedKeys.push('attributes');

339
packages/core/test/specs/data_sources/model/data_collection/ComponentDataCollection.getters-setters.ts

@ -0,0 +1,339 @@
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';
import ComponentDataCollection from '../../../../../src/data_sources/model/data_collection/ComponentDataCollection';
describe('Collection component getters and setters', () => {
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', firstName: 'Name1', age: '12' },
{ id: 'user2', user: 'user2', firstName: 'Name2', age: '14' },
{ id: 'user3', user: 'user3', firstName: 'Name3', age: '16' },
],
});
firstRecord = dataSource.getRecord('user1')!;
secondRecord = dataSource.getRecord('user2')!;
});
afterEach(() => {
em.destroy();
});
describe('Getters', () => {
let cmp: ComponentDataCollection;
beforeEach(() => {
cmp = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
components: [
{
type: 'default',
tagName: 'div',
attributes: {
'data-user': {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'my_collection',
path: 'user',
},
},
},
],
},
collectionConfig: {
collectionId: 'my_collection',
startIndex: 1,
endIndex: 2,
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0] as ComponentDataCollection;
});
test('getItemsCount should return the correct number of items', () => {
expect(cmp.getItemsCount()).toBe(2);
});
test('getConfigStartIndex should return the correct start index', () => {
expect(cmp.getConfigStartIndex()).toBe(1);
});
test('getConfigEndIndex should return the correct end index', () => {
expect(cmp.getConfigEndIndex()).toBe(2);
});
test('getComponentDef should return the correct component definition', () => {
const componentDef = cmp.getComponentDef();
expect(componentDef.type).toBe('default');
expect(componentDef.components).toHaveLength(1);
expect(componentDef.attributes?.['data-user']).toEqual({
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'my_collection',
path: 'user',
});
});
test('getDataSource should return the correct data source', () => {
const ds = cmp.getDataSource();
expect(ds).toEqual({
type: DataVariableType,
path: 'my_data_source_id',
});
});
test('getCollectionId should return the correct collection ID', () => {
expect(cmp.getCollectionId()).toBe('my_collection');
});
test('getItemsCount should return 0 when no records are present', () => {
dataSource.removeRecord('user1');
dataSource.removeRecord('user2');
dataSource.removeRecord('user3');
expect(cmp.getItemsCount()).toBe(0);
});
test('getConfigStartIndex should handle zero as a valid start index', () => {
cmp.setStartIndex(0);
expect(cmp.getConfigStartIndex()).toBe(0);
expect(cmp.getItemsCount()).toBe(3);
});
test('getConfigEndIndex should handle zero as a valid end index', () => {
cmp.setEndIndex(0);
expect(cmp.getConfigEndIndex()).toBe(0);
expect(cmp.getItemsCount()).toBe(0);
});
});
describe('Setters', () => {
let cmp: ComponentDataCollection;
beforeEach(() => {
cmp = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
components: [
{
type: 'default',
tagName: 'div',
attributes: {
'data-user': {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'my_collection',
path: 'user',
},
},
},
],
},
collectionConfig: {
collectionId: 'my_collection',
startIndex: 1,
endIndex: 2,
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0] as ComponentDataCollection;
});
test('setComponentDef should update the component definition and reflect in children', () => {
const newComponentDef = {
type: 'newType',
components: [
{
type: 'default',
tagName: 'span',
attributes: {
'data-name': {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'my_collection',
path: 'firstName',
},
},
},
],
};
cmp.setComponentDef(newComponentDef);
const children = cmp.components();
expect(children).toHaveLength(2);
expect(children.at(0).get('type')).toBe('newType');
expect(children.at(0).components().at(0).get('tagName')).toBe('span');
expect(children.at(0).components().at(0).getAttributes()['data-name']).toBe('Name2');
});
test('setStartIndex should update the start index and reflect in children', () => {
cmp.setStartIndex(0);
expect(cmp.getConfigStartIndex()).toBe(0);
const children = cmp.components();
expect(children).toHaveLength(3);
expect(children.at(0).getAttributes()['data-user']).toBe('user1');
expect(children.at(1).getAttributes()['data-user']).toBe('user2');
expect(children.at(2).getAttributes()['data-user']).toBe('user3');
});
test('setEndIndex should update the end index and reflect in children', () => {
cmp.setEndIndex(3);
expect(cmp.getConfigEndIndex()).toBe(3);
const children = cmp.components();
expect(children).toHaveLength(2);
expect(children.at(0).getAttributes()['data-user']).toBe('user2');
expect(children.at(1).getAttributes()['data-user']).toBe('user3');
});
test('setDataSource should update the data source and reflect in children', () => {
dsm.add({
id: 'new_data_source_id',
records: [
{ id: 'user4', user: 'user4', firstName: 'Name4', age: '20' },
],
});
cmp.setDataSource({
type: DataVariableType,
path: 'new_data_source_id',
});
const children = cmp.components();
expect(children).toHaveLength(1); // Only one record in the new data source
expect(children.at(0).getAttributes()['data-user']).toBe('user4');
});
test('setStartIndex with zero should include the first record', () => {
cmp.setStartIndex(0);
const children = cmp.components();
expect(children).toHaveLength(3); // Includes all records
expect(children.at(0).getAttributes()['data-user']).toBe('user1');
});
test('setEndIndex with zero should result in no children', () => {
cmp.setEndIndex(0);
const children = cmp.components();
expect(children).toHaveLength(0); // No records included
});
test('setDataSource with an empty data source should result in no children', () => {
const emptyDataSource = dsm.add({
id: 'empty_data_source_id',
records: [],
});
cmp.setDataSource({
type: DataVariableType,
path: 'empty_data_source_id',
});
const children = cmp.components();
expect(children).toHaveLength(0); // No records in the data source
});
});
describe('Impact on HTML output', () => {
let cmp: ComponentDataCollection;
beforeEach(() => {
cmp = wrapper.components({
type: DataCollectionType,
collectionDef: {
componentDef: {
type: 'default',
components: [
{
type: 'default',
tagName: 'div',
attributes: {
'data-user': {
type: DataCollectionVariableType,
variableType: DataCollectionStateVariableType.currentItem,
collectionId: 'my_collection',
path: 'user',
},
},
},
],
},
collectionConfig: {
collectionId: 'my_collection',
startIndex: 1,
endIndex: 2,
dataSource: {
type: DataVariableType,
path: 'my_data_source_id',
},
},
},
})[0] as ComponentDataCollection;
});
test('HTML output should reflect changes in startIndex', () => {
cmp.setStartIndex(0);
const html = cmp.toHTML();
expect(html).toContain('data-user="user1"');
expect(html).toContain('data-user="user2"');
expect(html).toContain('data-user="user3"');
});
test('HTML output should reflect changes in endIndex', () => {
cmp.setEndIndex(3);
const html = cmp.toHTML();
expect(html).toContain('data-user="user2"');
expect(html).toContain('data-user="user3"');
});
test('HTML output should reflect changes in dataSource', () => {
dsm.add({
id: 'new_data_source_id',
records: [
{ id: 'user4', user: 'user4', firstName: 'Name4', age: '20' },
],
});
cmp.setDataSource({
type: DataVariableType,
path: 'new_data_source_id',
});
const html = cmp.toHTML();
expect(html).toContain('data-user="user4"');
});
test('HTML output should be empty when endIndex is zero', () => {
cmp.setEndIndex(0);
const html = cmp.toHTML();
expect(html).not.toContain('data-user');
});
});
});
Loading…
Cancel
Save