Browse Source

refactor: bring back getValye, getContext and fromPath, add nested tests

feature/data-sources
danstarns 2 years ago
parent
commit
da44545dcd
  1. 50
      docs/api/data_source_manager.md
  2. 70
      src/data_sources/index.ts
  3. 5
      src/data_sources/model/ComponentDataVariable.ts
  4. 14
      src/data_sources/view/ComponentDataVariableView.ts
  5. 16
      src/utils/mixins.ts
  6. 304
      test/specs/data_sources/model/ComponentDataVariable.ts

50
docs/api/data_source_manager.md

@ -77,6 +77,35 @@ const ds = dsm.get('my_data_source_id');
Returns **[DataSource]** Data source. 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
Remove data source. Remove data source.
@ -94,6 +123,27 @@ const removed = dsm.remove('DS_ID');
Returns **[DataSource]** Removed data source. 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 [1]: #add
[2]: #get [2]: #get

70
src/data_sources/index.ts

@ -36,8 +36,10 @@
*/ */
import { ItemManagerModule, ModuleConfig } from '../abstract/Module'; import { ItemManagerModule, ModuleConfig } from '../abstract/Module';
import { AddOptions, RemoveOptions } from '../common'; import { AddOptions, ObjectAny, RemoveOptions } from '../common';
import EditorModel from '../editor/model/Editor'; import EditorModel from '../editor/model/Editor';
import { get, stringToPath } from '../utils/mixins';
import DataRecord from './model/DataRecord';
import DataSource from './model/DataSource'; import DataSource from './model/DataSource';
import DataSources from './model/DataSources'; import DataSources from './model/DataSources';
import { DataSourcesEvents, DataSourceProps } from './types'; import { DataSourcesEvents, DataSourceProps } from './types';
@ -83,6 +85,44 @@ export default class DataSourceManager extends ItemManagerModule<ModuleConfig, D
return this.all.get(id); return this.all.get(id);
} }
/**
* Get value from data sources by key
* @param {String} key Path to value.
* @param {any} defValue
* @returns {any}
* const value = dsm.getValue('ds_id.record_id.propName', 'defaultValue');
*/
getValue(key: string | string[], defValue: any) {
return get(this.getContext(), key, defValue);
}
/**
* 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.
*
* @returns {ObjectAny} - The context of all data sources, with transformed records.
* @example
* const context = dsm.getContext();
* // e.g., { dataSourceId: { 0: { id: 'record1', name: 'value1' }, record1: { id: 'record1', name: 'value1' } } }
*/
getContext() {
return this.all.reduce((acc, ds) => {
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. * Remove data source.
* @param {String|[DataSource]} id Id of the data source. * @param {String|[DataSource]} id Id of the data source.
@ -93,4 +133,32 @@ export default class DataSourceManager extends ItemManagerModule<ModuleConfig, D
remove(id: string | DataSource, opts?: RemoveOptions) { remove(id: string | DataSource, opts?: RemoveOptions) {
return this.__remove(id, opts); return this.__remove(id, opts);
} }
/**
* 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.
*
* @param {String} path - The string path in the format 'dataSourceId.recordId.property'.
* @returns {[DataSource?, DataRecord?, String?]} - An array containing the data source,
* data record, and optional property path.
* @example
* const [dataSource, dataRecord, propPath] = dsm.fromPath('my_data_source_id.record_id.myProp');
* // e.g., [DataSource, DataRecord, 'myProp']
*/
fromPath(path: string) {
const result: [DataSource?, DataRecord?, string?] = [];
const [dsId, drId, ...resPath] = stringToPath(path || '');
const dataSource = this.get(dsId);
const dataRecord = dataSource?.records.get(drId);
dataSource && result.push(dataSource);
if (dataRecord) {
result.push(dataRecord);
resPath.length && result.push(resPath.join('.'));
}
return result;
}
} }

5
src/data_sources/model/ComponentDataVariable.ts

@ -16,11 +16,8 @@ export default class ComponentDataVariable extends Component {
getInnerHTML(opts: ToHTMLOptions & { keepVariables?: boolean } = {}) { getInnerHTML(opts: ToHTMLOptions & { keepVariables?: boolean } = {}) {
const { path, value } = this.attributes; const { path, value } = this.attributes;
const [dsId, drId, key] = stringToPath(path);
const ds = this.em.DataSources.get(dsId);
const dr = ds && ds.getRecord(drId);
return opts.keepVariables ? path : dr ? dr.get(key) : value; return opts.keepVariables ? path : this.em.DataSources.getValue(path, value);
} }
static isComponent(el: HTMLElement) { static isComponent(el: HTMLElement) {

14
src/data_sources/view/ComponentDataVariableView.ts

@ -16,11 +16,8 @@ export default class ComponentDataVariableView extends ComponentView<ComponentDa
const { model, em } = this; const { model, em } = this;
const { path } = model.attributes; const { path } = model.attributes;
const normPath = stringToPath(path || '').join('.'); const normPath = stringToPath(path || '').join('.');
const [dsId, drId] = stringToPath(path || '');
const { DataSources } = em; const { DataSources } = em;
const ds = DataSources.get(dsId); const [ds, dr] = DataSources.fromPath(path);
const dr = ds && ds.getRecord(drId);
const dataListeners: DataVariableListener[] = []; const dataListeners: DataVariableListener[] = [];
const prevListeners = this.dataListeners || []; const prevListeners = this.dataListeners || [];
@ -41,14 +38,7 @@ export default class ComponentDataVariableView extends ComponentView<ComponentDa
postRender() { postRender() {
const { model, el, em } = this; const { model, el, em } = this;
const { path, value } = model.attributes; const { path, value } = model.attributes;
const { DataSources } = em; el.innerHTML = em.DataSources.getValue(path, value);
const [dsId, drId, key, ...resPath] = stringToPath(path || '');
const ds = DataSources.get(dsId);
const dr = ds && ds.getRecord(drId);
el.innerHTML = dr ? dr.get(key) : value;
super.postRender(); super.postRender();
} }
} }

16
src/utils/mixins.ts

@ -20,6 +20,22 @@ export const stringToPath = function (string: string) {
return result; return result;
}; };
function castPath(value: string | string[], object: ObjectAny) {
if (isArray(value)) return value;
return object.hasOwnProperty(value) ? [value] : stringToPath(value);
}
export const get = (object: ObjectAny, path: string | string[], def: any) => {
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 isBultInMethod = (key: string) => isFunction(obj[key]);
export const normalizeKey = (key: string) => (isBultInMethod(key) ? `_${key}` : key); export const normalizeKey = (key: string) => (isBultInMethod(key) ? `_${key}` : key);

304
test/specs/data_sources/model/ComponentDataVariable.ts

@ -1,8 +1,6 @@
import Editor from '../../../../src/editor/model/Editor'; import Editor from '../../../../src/editor/model/Editor';
import DataSourceManager from '../../../../src/data_sources'; import DataSourceManager from '../../../../src/data_sources';
import { DataSourcesEvents } from '../../../../src/data_sources/types';
import ComponentWrapper from '../../../../src/dom_components/model/ComponentWrapper'; 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 { DataVariableType } from '../../../../src/data_sources/model/DataVariable';
import { DataSourceProps } from '../../../../src/data_sources/types'; import { DataSourceProps } from '../../../../src/data_sources/types';
@ -12,23 +10,6 @@ describe('ComponentDataVariable', () => {
let fixtures: HTMLElement; let fixtures: HTMLElement;
let cmpRoot: ComponentWrapper; let cmpRoot: ComponentWrapper;
const addDataVariable = (path = 'ds1.id1.name') =>
cmpRoot.append<ComponentDataVariable>({
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(() => { beforeEach(() => {
em = new Editor({ em = new Editor({
mediaCondition: 'max-width', mediaCondition: 'max-width',
@ -53,133 +34,200 @@ describe('ComponentDataVariable', () => {
em.destroy(); em.destroy();
}); });
describe('Export', () => { test('component initializes with data-variable content', () => {
test('component exports properly with default value', () => { const dataSource: DataSourceProps = {
const cmpVar = addDataVariable(); id: 'ds1',
expect(cmpVar.toHTML()).toBe('<div>default</div>'); records: [{ id: 'id1', name: 'Name1' }],
}); };
dsm.add(dataSource);
test('component exports properly with current value', () => {
addDataSource(); const cmp = cmpRoot.append({
const cmpVar = addDataVariable(); tagName: 'div',
expect(cmpVar.toHTML()).toBe('<div>Name1</div>'); type: 'default',
}); components: [
{
type: DataVariableType,
value: 'default',
path: 'ds1.id1.name',
},
],
})[0];
test('component exports properly with variable', () => { expect(cmp.getEl()?.innerHTML).toContain('Name1');
addDataSource();
const cmpVar = addDataVariable();
expect(cmpVar.getInnerHTML({ keepVariables: true })).toBe('ds1.id1.name');
});
}); });
test('component is properly initiliazed with default value', () => { test('component updates on data-variable change', () => {
const cmpVar = addDataVariable(); const dataSource: DataSourceProps = {
expect(cmpVar.getEl()?.innerHTML).toBe('default'); 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', () => { test("component uses default value if data source doesn't exist", () => {
addDataSource(); const cmp = cmpRoot.append({
const cmpVar = addDataVariable(); tagName: 'div',
expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); 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', () => { test('component updates on data source reset', () => {
const cmpVar = addDataVariable(); const dataSource: DataSourceProps = {
cmpVar.set({ value: 'none' }); id: 'ds3',
expect(cmpVar.getEl()?.innerHTML).toBe('none'); 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', () => { test('component updates on record removal', () => {
const eventFn1 = jest.fn(); const dataSource: DataSourceProps = {
const eventFn2 = jest.fn(); id: 'ds4',
const ds = addDataSource(); records: [{ id: 'id1', name: 'Name1' }],
const cmpVar = addDataVariable(); };
const el = cmpVar.getEl()!; dsm.add(dataSource);
const pathEvent = DataSourcesEvents.path;
const cmp = cmpRoot.append({
cmpVar.set({ path: 'ds1.id2.name' }); tagName: 'div',
expect(el.innerHTML).toBe('Name2'); type: 'default',
em.on(`${pathEvent}:ds1.id2.name`, eventFn1); components: [
ds.getRecord('id2')?.set({ name: 'Name2-UP' }); {
type: DataVariableType,
cmpVar.set({ path: 'ds1[id3]name' }); value: 'default',
expect(el.innerHTML).toBe('Name3'); path: 'ds4.id1.name',
em.on(`${pathEvent}:ds1.id3.name`, eventFn2); },
ds.getRecord('id3')?.set({ name: 'Name3-UP' }); ],
})[0];
expect(el.innerHTML).toBe('Name3-UP');
expect(eventFn1).toBeCalledTimes(1); expect(cmp.getEl()?.innerHTML).toContain('Name1');
expect(eventFn2).toBeCalledTimes(1);
const ds = dsm.get('ds4');
ds.removeRecord('id1');
expect(cmp.getEl()?.innerHTML).toContain('default');
}); });
describe('DataSource changes', () => { test('component initializes and updates with data-variable for nested object', () => {
test('component is properly updating on data source add', () => { const dataSource: DataSourceProps = {
const eventFn = jest.fn(); id: 'dsNestedObject',
em.on(DataSourcesEvents.add, eventFn); records: [
const cmpVar = addDataVariable(); {
const ds = addDataSource(); id: 'id1',
expect(eventFn).toBeCalledTimes(1); nestedObject: {
expect(eventFn).toBeCalledWith(ds, expect.any(Object)); name: 'NestedName1',
expect(cmpVar.getEl()?.innerHTML).toBe('Name1'); },
}); },
],
};
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', () => { expect(cmp.getEl()?.innerHTML).toContain('NestedName1');
addDataSource();
const cmpVar = addDataVariable();
const el = cmpVar.getEl()!;
expect(el.innerHTML).toBe('Name1');
dsm.all.reset();
expect(el.innerHTML).toBe('default');
});
test('component is properly updating on data source remove', () => { const ds = dsm.get('dsNestedObject');
const eventFn = jest.fn(); ds.getRecord('id1')?.set({ nestedObject: { name: 'NestedName1-UP' } });
em.on(DataSourcesEvents.remove, eventFn);
const ds = addDataSource(); expect(cmp.getEl()?.innerHTML).toContain('NestedName1-UP');
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');
});
}); });
describe('DataRecord changes', () => { test('component initializes and updates with data-variable for nested object inside an array', () => {
test('component is properly updating on record add', () => { const dataSource: DataSourceProps = {
const ds = addDataSource(); id: 'dsNestedArray',
const cmpVar = addDataVariable('ds1[id4]name'); records: [
const eventFn = jest.fn(); {
em.on(`${DataSourcesEvents.path}:ds1.id4.name`, eventFn); id: 'id1',
const newRecord = ds.addRecord({ id: 'id4', name: 'Name4' }); items: [
expect(cmpVar.getEl()?.innerHTML).toBe('Name4'); {
newRecord.set({ name: 'up' }); id: 'item1',
expect(cmpVar.getEl()?.innerHTML).toBe('up'); nestedObject: {
expect(eventFn).toBeCalledTimes(1); 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', () => { expect(cmp.getEl()?.innerHTML).toContain('NestedItemName1');
const ds = addDataSource();
const cmpVar = addDataVariable();
const el = cmpVar.getEl()!;
ds.getRecord('id1')?.set({ name: 'Name1-UP' });
expect(el.innerHTML).toBe('Name1-UP');
});
test('component is properly updating on record remove', () => { const ds = dsm.get('dsNestedArray');
const ds = addDataSource(); ds.getRecord('id1')?.set({
const cmpVar = addDataVariable(); items: [
const el = cmpVar.getEl()!; {
ds.removeRecord('id1'); id: 'item1',
expect(el.innerHTML).toBe('default'); nestedObject: { name: 'NestedItemName1-UP' },
},
],
}); });
test('component is properly updating on record reset', () => { expect(cmp.getEl()?.innerHTML).toContain('NestedItemName1-UP');
const ds = addDataSource();
const cmpVar = addDataVariable();
const el = cmpVar.getEl()!;
ds.records.reset();
expect(el.innerHTML).toBe('default');
});
}); });
}); });

Loading…
Cancel
Save