diff --git a/packages/core/src/data_sources/index.ts b/packages/core/src/data_sources/index.ts index 0c0c69c96..a23e50709 100644 --- a/packages/core/src/data_sources/index.ts +++ b/packages/core/src/data_sources/index.ts @@ -58,6 +58,7 @@ export default class DataSourceManager extends ItemManagerModule }) { - return get(opts?.context || this.getContext(), path, defValue); + if (opts?.context) return get(opts.context, path, defValue); + + const value = this.getValueFromDataSources(path); + + return value === undefined ? defValue : value; } /** @@ -140,18 +145,15 @@ export default class DataSourceManager extends ItemManagerModule { - acc[ds.id] = ds.records.reduce((accR, dr, i) => { - const dataRecord = dr; + if (!this.contextCache) { + this.contextCache = this.all.reduce((acc, ds) => { + acc[ds.id] = ds.getContext(); - const attributes = { ...dataRecord.attributes }; - delete attributes.__p; - accR[dataRecord.id || i] = attributes; - - return accR; + return acc; }, {} as ObjectAny); - return acc; - }, {} as ObjectAny); + } + + return this.contextCache; } /** @@ -182,7 +184,7 @@ export default class DataSourceManager extends ItemManagerModule { + this.contextCache = undefined; + }; + + private getValueFromDataSources(path: string | string[]) { + const pathParts = Array.isArray(path) ? path : stringToPath(path || ''); + if (!pathParts.length) return undefined; + + const [dsId, drId, ...resPath] = pathParts; + const dataSource = this.get(dsId); + + if (!dataSource) return undefined; + if (drId === undefined) return dataSource.getContext(); + + const dataRecord = dataSource.getRecord(drId); + if (!dataRecord) return undefined; + + const recordContext = dataRecord.getContext(); + + return resPath.length ? get(recordContext, resPath) : recordContext; + } + /** * Store data sources to a JSON object. * @returns {Array} Stored data sources. diff --git a/packages/core/src/data_sources/model/DataRecord.ts b/packages/core/src/data_sources/model/DataRecord.ts index d0ce687e3..a5f94de8b 100644 --- a/packages/core/src/data_sources/model/DataRecord.ts +++ b/packages/core/src/data_sources/model/DataRecord.ts @@ -56,6 +56,13 @@ export default class DataRecord ext return this.cl.indexOf(this); } + getContext() { + const attributes = { ...this.attributes }; + delete attributes.__p; + + return attributes; + } + /** * Handles changes to the record's attributes. * This method triggers a change event for each property that has been altered. diff --git a/packages/core/src/data_sources/model/DataRecords.ts b/packages/core/src/data_sources/model/DataRecords.ts index 1eadfcc22..08952501c 100644 --- a/packages/core/src/data_sources/model/DataRecords.ts +++ b/packages/core/src/data_sources/model/DataRecords.ts @@ -10,6 +10,24 @@ export default class DataRecords ex super(models, options); this.dataSource = options.dataSource; } + + getRecord(id: string | number): DataRecord | undefined { + return this.get(id) || this.getRecordByIndex(id); + } + + isIndexKey(id: string | number) { + return !this.get(id) && !!this.getRecordByIndex(id); + } + + private getRecordByIndex(id: string | number) { + const index = this.getIndex(id); + return index === undefined ? undefined : this.at(index); + } + + private getIndex(id: string | number) { + const index = typeof id === 'number' ? id : Number(id); + return Number.isInteger(index) && `${index}` === `${id}` && index >= 0 ? index : undefined; + } } DataRecords.prototype.model = DataRecord; diff --git a/packages/core/src/data_sources/model/DataSource.ts b/packages/core/src/data_sources/model/DataSource.ts index 9d9909738..e70823014 100644 --- a/packages/core/src/data_sources/model/DataSource.ts +++ b/packages/core/src/data_sources/model/DataSource.ts @@ -171,7 +171,11 @@ export default class DataSource this.getRecord(record.id)!); } + getContext() { + return this.records.reduce( + (acc, dataRecord, index) => { + acc[dataRecord.id || index] = dataRecord.getContext(); + return acc; + }, + {} as Record>, + ); + } + /** * Retrieves all records from the data source with resolved relations based on the schema. */ @@ -317,7 +331,9 @@ export default class DataSource { this.model = (props: DataSourceProps, opts = {}) => { return new DataSource(props, { ...opts, em }); }; + + this.on(collectionEvents, () => em.DataSources.invalidateContextCache()); } } diff --git a/packages/core/test/specs/data_sources/dynamic_values/styles.ts b/packages/core/test/specs/data_sources/dynamic_values/styles.ts index 39f2ce4a3..d21e6daa8 100644 --- a/packages/core/test/specs/data_sources/dynamic_values/styles.ts +++ b/packages/core/test/specs/data_sources/dynamic_values/styles.ts @@ -158,6 +158,38 @@ describe('StyleDataVariable', () => { expect(updatedStyle).toHaveProperty('color', 'blue'); }); + test('component style binding ignores unrelated record add/remove churn', () => { + dsm.add({ + id: 'style-churn', + records: [ + { id: 'bound-record', color: 'red' }, + { id: 'other-record', color: 'blue' }, + ], + }); + + const cmp = cmpRoot.append({ + tagName: 'h1', + type: 'text', + content: 'Hello World', + style: { + color: { + type: DataVariableType, + defaultValue: 'black', + path: 'style-churn.bound-record.color', + }, + }, + })[0]; + + const addStyleSpy = jest.spyOn(cmp, 'addStyle'); + const ds = dsm.get('style-churn'); + + ds.addRecord({ id: 'new-record', color: 'green' }); + ds.removeRecord('other-record'); + + expect(addStyleSpy).not.toHaveBeenCalled(); + expect(cmp.getStyle()).toHaveProperty('color', 'red'); + }); + describe('Component style manipulations', () => { test('adding a new dynamic style with addStyle', () => { dsm.add({ id: 'data1', records: [{ id: 'rec1', color: 'red' }] }); diff --git a/packages/core/test/specs/data_sources/index.ts b/packages/core/test/specs/data_sources/index.ts index 9584753ef..b9132f9e0 100644 --- a/packages/core/test/specs/data_sources/index.ts +++ b/packages/core/test/specs/data_sources/index.ts @@ -82,6 +82,34 @@ describe('DataSourceManager', () => { expect(dsm.getValue(`ds1.id4.metadata.roles`)).toEqual(roles); expect(dsm.getValue(`ds1.id4.metadata.roles[1]`)).toEqual(roles[1]); }); + + test('with index-based record path', () => { + dsm.add({ + id: 'recordsByIndex', + records: [{ name: 'First item' }, { name: 'Second item' }] as any, + }); + + expect(dsm.getValue('recordsByIndex.0.name')).toBe('First item'); + expect(dsm.getValue('recordsByIndex.1.name')).toBe('Second item'); + }); + }); + + describe('getContext', () => { + test('memoizes until data changes', () => { + const ds = addDataSource(); + + const contextA = dsm.getContext(); + const contextB = dsm.getContext(); + + expect(contextA).toBe(contextB); + + ds.getRecord('id1')?.set({ name: 'Name1 updated' }); + + const contextC = dsm.getContext(); + + expect(contextC).not.toBe(contextA); + expect(contextC.ds1.id1.name).toBe('Name1 updated'); + }); }); describe('setValue', () => {