From da44545dcd47f10755d5bb20e15d202ea863ca6d Mon Sep 17 00:00:00 2001 From: danstarns Date: Tue, 27 Aug 2024 21:24:35 -0700 Subject: [PATCH] refactor: bring back getValye, getContext and fromPath, add nested tests --- docs/api/data_source_manager.md | 50 +++ src/data_sources/index.ts | 70 +++- .../model/ComponentDataVariable.ts | 5 +- .../view/ComponentDataVariableView.ts | 14 +- src/utils/mixins.ts | 16 + .../model/ComponentDataVariable.ts | 304 ++++++++++-------- 6 files changed, 314 insertions(+), 145 deletions(-) diff --git a/docs/api/data_source_manager.md b/docs/api/data_source_manager.md index 9151256cf..92c99152f 100644 --- a/docs/api/data_source_manager.md +++ b/docs/api/data_source_manager.md @@ -77,6 +77,35 @@ const ds = dsm.get('my_data_source_id'); Returns **[DataSource]** Data source. +## getValue + +Get value from data sources by key + +### Parameters + +* `key` **[String][7]** Path to value. +* `defValue` **any** + +Returns **any** const value = dsm.getValue('ds\_id.record\_id.propName', 'defaultValue'); + +## getContext + +Retrieve the entire context of data sources. +This method aggregates all data records from all data sources and applies any +`onRecordRead` transformers defined within each data source. The result is an +object representing the current state of all data sources, where each data source +ID maps to an object containing its records' attributes. Each record is keyed by +both its index and its ID. + +### Examples + +```javascript +const context = dsm.getContext(); +// e.g., { dataSourceId: { 0: { id: 'record1', name: 'value1' }, record1: { id: 'record1', name: 'value1' } } } +``` + +Returns **ObjectAny** The context of all data sources, with transformed records. + ## remove Remove data source. @@ -94,6 +123,27 @@ const removed = dsm.remove('DS_ID'); Returns **[DataSource]** Removed data source. +## fromPath + +Retrieve a data source, data record, and optional property path based on a string path. +This method parses a string path to identify and retrieve the corresponding data source +and data record. If a property path is included in the input, it will also be returned. +The method is useful for accessing nested data within data sources. + +### Parameters + +* `path` **[String][7]** The string path in the format 'dataSourceId.recordId.property'. + +### Examples + +```javascript +const [dataSource, dataRecord, propPath] = dsm.fromPath('my_data_source_id.record_id.myProp'); +// e.g., [DataSource, DataRecord, 'myProp'] +``` + +Returns **[DataSource?, DataRecord?, [String][7]?]** An array containing the data source, +data record, and optional property path. + [1]: #add [2]: #get diff --git a/src/data_sources/index.ts b/src/data_sources/index.ts index 09fdb1145..f2c3eaa05 100644 --- a/src/data_sources/index.ts +++ b/src/data_sources/index.ts @@ -36,8 +36,10 @@ */ import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; -import { AddOptions, RemoveOptions } from '../common'; +import { AddOptions, ObjectAny, RemoveOptions } from '../common'; import EditorModel from '../editor/model/Editor'; +import { get, stringToPath } from '../utils/mixins'; +import DataRecord from './model/DataRecord'; import DataSource from './model/DataSource'; import DataSources from './model/DataSources'; import { DataSourcesEvents, DataSourceProps } from './types'; @@ -83,6 +85,44 @@ export default class DataSourceManager extends ItemManagerModule { + acc[ds.id] = ds.records.reduce((accR, dr, i) => { + const dataRecord = ds.transformers.onRecordRead ? ds.transformers.onRecordRead({ record: dr }) : dr; + + accR[i] = dataRecord.attributes; + accR[dataRecord.id || i] = dataRecord.attributes; + + return accR; + }, {} as ObjectAny); + return acc; + }, {} as ObjectAny); + } + /** * Remove data source. * @param {String|[DataSource]} id Id of the data source. @@ -93,4 +133,32 @@ export default class DataSourceManager extends ItemManagerModule { + const paths = castPath(path, object); + const length = paths.length; + let index = 0; + + while (object != null && index < length) { + object = object[`${paths[index++]}`]; + } + return (index && index == length ? object : undefined) ?? def; +}; + export const isBultInMethod = (key: string) => isFunction(obj[key]); export const normalizeKey = (key: string) => (isBultInMethod(key) ? `_${key}` : key); diff --git a/test/specs/data_sources/model/ComponentDataVariable.ts b/test/specs/data_sources/model/ComponentDataVariable.ts index 0692f3427..fea380ed6 100644 --- a/test/specs/data_sources/model/ComponentDataVariable.ts +++ b/test/specs/data_sources/model/ComponentDataVariable.ts @@ -1,8 +1,6 @@ import Editor from '../../../../src/editor/model/Editor'; import DataSourceManager from '../../../../src/data_sources'; -import { DataSourcesEvents } from '../../../../src/data_sources/types'; import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; -import ComponentDataVariable from '../../../../src/data_sources/model/ComponentDataVariable'; import { DataVariableType } from '../../../../src/data_sources/model/DataVariable'; import { DataSourceProps } from '../../../../src/data_sources/types'; @@ -12,23 +10,6 @@ describe('ComponentDataVariable', () => { let fixtures: HTMLElement; let cmpRoot: ComponentWrapper; - const addDataVariable = (path = 'ds1.id1.name') => - cmpRoot.append({ - type: DataVariableType, - value: 'default', - path, - })[0]; - - const dsTest: DataSourceProps = { - id: 'ds1', - records: [ - { id: 'id1', name: 'Name1' }, - { id: 'id2', name: 'Name2' }, - { id: 'id3', name: 'Name3' }, - ], - }; - const addDataSource = () => dsm.add(dsTest); - beforeEach(() => { em = new Editor({ mediaCondition: 'max-width', @@ -53,133 +34,200 @@ describe('ComponentDataVariable', () => { em.destroy(); }); - describe('Export', () => { - test('component exports properly with default value', () => { - const cmpVar = addDataVariable(); - expect(cmpVar.toHTML()).toBe('
default
'); - }); - - test('component exports properly with current value', () => { - addDataSource(); - const cmpVar = addDataVariable(); - expect(cmpVar.toHTML()).toBe('
Name1
'); - }); + test('component initializes with data-variable content', () => { + const dataSource: DataSourceProps = { + id: 'ds1', + records: [{ id: 'id1', name: 'Name1' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'ds1.id1.name', + }, + ], + })[0]; - test('component exports properly with variable', () => { - addDataSource(); - const cmpVar = addDataVariable(); - expect(cmpVar.getInnerHTML({ keepVariables: true })).toBe('ds1.id1.name'); - }); + expect(cmp.getEl()?.innerHTML).toContain('Name1'); }); - test('component is properly initiliazed with default value', () => { - const cmpVar = addDataVariable(); - expect(cmpVar.getEl()?.innerHTML).toBe('default'); + test('component updates on data-variable change', () => { + const dataSource: DataSourceProps = { + id: 'ds2', + records: [{ id: 'id1', name: 'Name1' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'ds2.id1.name', + }, + ], + })[0]; + + expect(cmp.getEl()?.innerHTML).toContain('Name1'); + + const ds = dsm.get('ds2'); + ds.getRecord('id1')?.set({ name: 'Name1-UP' }); + + expect(cmp.getEl()?.innerHTML).toContain('Name1-UP'); }); - test('component is properly initiliazed with current value', () => { - addDataSource(); - const cmpVar = addDataVariable(); - expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); + test("component uses default value if data source doesn't exist", () => { + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'unknown.id1.name', + }, + ], + })[0]; + + expect(cmp.getEl()?.innerHTML).toContain('default'); }); - test('component is properly updating on its default value change', () => { - const cmpVar = addDataVariable(); - cmpVar.set({ value: 'none' }); - expect(cmpVar.getEl()?.innerHTML).toBe('none'); + test('component updates on data source reset', () => { + const dataSource: DataSourceProps = { + id: 'ds3', + records: [{ id: 'id1', name: 'Name1' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'ds3.id1.name', + }, + ], + })[0]; + + expect(cmp.getEl()?.innerHTML).toContain('Name1'); + + dsm.all.reset(); + expect(cmp.getEl()?.innerHTML).toContain('default'); }); - test('component is properly updating on its path change', () => { - const eventFn1 = jest.fn(); - const eventFn2 = jest.fn(); - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - const pathEvent = DataSourcesEvents.path; - - cmpVar.set({ path: 'ds1.id2.name' }); - expect(el.innerHTML).toBe('Name2'); - em.on(`${pathEvent}:ds1.id2.name`, eventFn1); - ds.getRecord('id2')?.set({ name: 'Name2-UP' }); - - cmpVar.set({ path: 'ds1[id3]name' }); - expect(el.innerHTML).toBe('Name3'); - em.on(`${pathEvent}:ds1.id3.name`, eventFn2); - ds.getRecord('id3')?.set({ name: 'Name3-UP' }); - - expect(el.innerHTML).toBe('Name3-UP'); - expect(eventFn1).toBeCalledTimes(1); - expect(eventFn2).toBeCalledTimes(1); + test('component updates on record removal', () => { + const dataSource: DataSourceProps = { + id: 'ds4', + records: [{ id: 'id1', name: 'Name1' }], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'ds4.id1.name', + }, + ], + })[0]; + + expect(cmp.getEl()?.innerHTML).toContain('Name1'); + + const ds = dsm.get('ds4'); + ds.removeRecord('id1'); + + expect(cmp.getEl()?.innerHTML).toContain('default'); }); - describe('DataSource changes', () => { - test('component is properly updating on data source add', () => { - const eventFn = jest.fn(); - em.on(DataSourcesEvents.add, eventFn); - const cmpVar = addDataVariable(); - const ds = addDataSource(); - expect(eventFn).toBeCalledTimes(1); - expect(eventFn).toBeCalledWith(ds, expect.any(Object)); - expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); - }); + test('component initializes and updates with data-variable for nested object', () => { + const dataSource: DataSourceProps = { + id: 'dsNestedObject', + records: [ + { + id: 'id1', + nestedObject: { + name: 'NestedName1', + }, + }, + ], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'dsNestedObject.id1.nestedObject.name', + }, + ], + })[0]; - test('component is properly updating on data source reset', () => { - addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - expect(el.innerHTML).toBe('Name1'); - dsm.all.reset(); - expect(el.innerHTML).toBe('default'); - }); + expect(cmp.getEl()?.innerHTML).toContain('NestedName1'); - test('component is properly updating on data source remove', () => { - const eventFn = jest.fn(); - em.on(DataSourcesEvents.remove, eventFn); - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - dsm.remove('ds1'); - expect(eventFn).toBeCalledTimes(1); - expect(eventFn).toBeCalledWith(ds, expect.any(Object)); - expect(el.innerHTML).toBe('default'); - }); + const ds = dsm.get('dsNestedObject'); + ds.getRecord('id1')?.set({ nestedObject: { name: 'NestedName1-UP' } }); + + expect(cmp.getEl()?.innerHTML).toContain('NestedName1-UP'); }); - describe('DataRecord changes', () => { - test('component is properly updating on record add', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable('ds1[id4]name'); - const eventFn = jest.fn(); - em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); - const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); - expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); - newRecord.set({ name: 'up' }); - expect(cmpVar.getEl()?.innerHTML).toBe('up'); - expect(eventFn).toBeCalledTimes(1); - }); + test('component initializes and updates with data-variable for nested object inside an array', () => { + const dataSource: DataSourceProps = { + id: 'dsNestedArray', + records: [ + { + id: 'id1', + items: [ + { + id: 'item1', + nestedObject: { + name: 'NestedItemName1', + }, + }, + ], + }, + ], + }; + dsm.add(dataSource); + + const cmp = cmpRoot.append({ + tagName: 'div', + type: 'default', + components: [ + { + type: DataVariableType, + value: 'default', + path: 'dsNestedArray.id1.items.0.nestedObject.name', + }, + ], + })[0]; - test('component is properly updating on record change', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - ds.getRecord('id1')?.set({ name: 'Name1-UP' }); - expect(el.innerHTML).toBe('Name1-UP'); - }); + expect(cmp.getEl()?.innerHTML).toContain('NestedItemName1'); - test('component is properly updating on record remove', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - ds.removeRecord('id1'); - expect(el.innerHTML).toBe('default'); + const ds = dsm.get('dsNestedArray'); + ds.getRecord('id1')?.set({ + items: [ + { + id: 'item1', + nestedObject: { name: 'NestedItemName1-UP' }, + }, + ], }); - test('component is properly updating on record reset', () => { - const ds = addDataSource(); - const cmpVar = addDataVariable(); - const el = cmpVar.getEl()!; - ds.records.reset(); - expect(el.innerHTML).toBe('default'); - }); + expect(cmp.getEl()?.innerHTML).toContain('NestedItemName1-UP'); }); });