Browse Source

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

pull/6018/head
danstarns 1 year 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.
## 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

70
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<ModuleConfig, D
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.
* @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) {
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 } = {}) {
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) {

14
src/data_sources/view/ComponentDataVariableView.ts

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

16
src/utils/mixins.ts

@ -20,6 +20,22 @@ export const stringToPath = function (string: string) {
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 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 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<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(() => {
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('<div>default</div>');
});
test('component exports properly with current value', () => {
addDataSource();
const cmpVar = addDataVariable();
expect(cmpVar.toHTML()).toBe('<div>Name1</div>');
});
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');
});
});

Loading…
Cancel
Save