diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/__snapshots__/options.test.ts.snap b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/__snapshots__/options.test.ts.snap new file mode 100644 index 000000000000..bdab937fa145 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/__snapshots__/options.test.ts.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Options onDataErrorOccurred should be called when load error happens 1`] = ` +[ + { + "component": Any, + "error": [Error: my error], + }, +] +`; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts new file mode 100644 index 000000000000..88d56c4a01bb --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/data_controller.ts @@ -0,0 +1,144 @@ +/* eslint-disable @typescript-eslint/no-invalid-void-type */ +/* eslint-disable no-param-reassign */ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import type { DataSource } from '@js/common/data'; +import type { SubsGets } from '@ts/core/reactive/index'; +import { + computed, effect, state, +} from '@ts/core/reactive/index'; +import { createPromise } from '@ts/core/utils/promise'; + +import { OptionsController } from '../options_controller/options_controller'; +import type { DataObject, Key } from './types'; +import { normalizeDataSource, updateItemsImmutable } from './utils'; + +export class DataController { + private readonly loadedPromise = createPromise(); + + private readonly dataSourceConfiguration = this.options.oneWay('dataSource'); + + private readonly keyExpr = this.options.oneWay('keyExpr'); + + public readonly dataSource = computed( + (dataSourceLike, keyExpr) => normalizeDataSource(dataSourceLike, keyExpr), + [this.dataSourceConfiguration, this.keyExpr], + ); + + // TODO + private readonly cacheEnabled = this.options.oneWay('cacheEnabled'); + + private readonly pagingEnabled = this.options.twoWay('paging.enabled'); + + public readonly pageIndex = this.options.twoWay('paging.pageIndex'); + + public readonly pageSize = this.options.twoWay('paging.pageSize'); + + // TODO + private readonly remoteOperations = this.options.oneWay('remoteOperations'); + + private readonly onDataErrorOccurred = this.options.action('onDataErrorOccurred'); + + private readonly _items = state([]); + + public readonly items: SubsGets = this._items; + + private readonly _totalCount = state(0); + + public readonly totalCount: SubsGets = this._totalCount; + + public readonly isLoading = state(false); + + public readonly pageCount = computed( + (totalCount, pageSize) => Math.ceil(totalCount / pageSize), + [this.totalCount, this.pageSize], + ); + + public static dependencies = [OptionsController] as const; + + constructor( + private readonly options: OptionsController, + ) { + effect( + (dataSource) => { + const changedCallback = (e?): void => { + this.onChanged(dataSource, e); + }; + const loadingChangedCallback = (): void => { + this.isLoading.update(dataSource.isLoading()); + }; + const loadErrorCallback = (error: string): void => { + const callback = this.onDataErrorOccurred.unreactive_get(); + callback({ error }); + changedCallback(); + }; + + if (dataSource.isLoaded()) { + changedCallback(); + } + dataSource.on('changed', changedCallback); + dataSource.on('loadingChanged', loadingChangedCallback); + dataSource.on('loadError', loadErrorCallback); + + return (): void => { + dataSource.off('changed', changedCallback); + dataSource.off('loadingChanged', loadingChangedCallback); + dataSource.off('loadError', loadErrorCallback); + }; + }, + [this.dataSource], + ); + + effect( + (dataSource, pageIndex, pageSize, pagingEnabled) => { + let someParamChanged = false; + if (dataSource.pageIndex() !== pageIndex) { + dataSource.pageIndex(pageIndex); + someParamChanged ||= true; + } + if (dataSource.pageSize() !== pageSize) { + dataSource.pageSize(pageSize); + someParamChanged ||= true; + } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare + if (dataSource.requireTotalCount() !== true) { + dataSource.requireTotalCount(true); + someParamChanged ||= true; + } + if (dataSource.paginate() !== pagingEnabled) { + dataSource.paginate(pagingEnabled); + someParamChanged ||= true; + } + + if (someParamChanged || !dataSource.isLoaded()) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + dataSource.load(); + } + }, + [this.dataSource, this.pageIndex, this.pageSize, this.pagingEnabled], + ); + } + + private onChanged(dataSource: DataSource, e): void { + let items = dataSource.items() as DataObject[]; + + if (e?.changes) { + items = this._items.unreactive_get(); + items = updateItemsImmutable(items, e.changes, dataSource.store()); + } + + this._items.update(items); + this.pageIndex.update(dataSource.pageIndex()); + this.pageSize.update(dataSource.pageSize()); + this._totalCount.update(dataSource.totalCount()); + this.loadedPromise.resolve(); + } + + public getDataKey(data: DataObject): Key { + return this.dataSource.unreactive_get().store().keyOf(data); + } + + public waitLoaded(): Promise { + return this.loadedPromise.promise; + } +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/index.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/index.ts new file mode 100644 index 000000000000..11e3ebad75f5 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/index.ts @@ -0,0 +1,3 @@ +export { DataController } from './data_controller'; +export { defaultOptions, type Options } from './options'; +export { PublicMethods } from './public_methods'; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.test.ts new file mode 100644 index 000000000000..6494e74ad5e9 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.test.ts @@ -0,0 +1,235 @@ +/* eslint-disable spellcheck/spell-checker */ +import { + afterAll, + beforeAll, + describe, expect, it, jest, +} from '@jest/globals'; +import { CustomStore } from '@js/common/data'; +import DataSource from '@js/data/data_source'; +import { logger } from '@ts/core/utils/m_console'; +import ArrayStore from '@ts/data/m_array_store'; + +import type { Options } from '../options'; +import { OptionsControllerMock } from '../options_controller/options_controller.mock'; +import { DataController } from './data_controller'; + +beforeAll(() => { + jest.spyOn(logger, 'error').mockImplementation(() => {}); +}); +afterAll(() => { + jest.restoreAllMocks(); +}); + +const setup = (options: Options) => { + const optionsController = new OptionsControllerMock(options); + const dataController = new DataController(optionsController); + + return { + optionsController, + dataController, + }; +}; + +describe('Options', () => { + describe('cacheEnabled', () => { + const setupForCacheEnabled = ({ cacheEnabled }) => { + const store = new ArrayStore({ + data: [ + { id: 1, value: 'value 1' }, + { id: 2, value: 'value 2' }, + { id: 3, value: 'value 3' }, + ], + key: 'id', + }); + + jest.spyOn(store, 'load'); + + const { dataController } = setup({ + cacheEnabled, + dataSource: store, + paging: { + pageSize: 1, + }, + }); + + return { store, dataController }; + }; + + describe('when it is false', () => { + it('should skip caching requests', () => { + const { store, dataController } = setupForCacheEnabled({ + cacheEnabled: false, + }); + expect(store.load).toBeCalledTimes(1); + + dataController.pageIndex.update(1); + expect(store.load).toBeCalledTimes(2); + + dataController.pageIndex.update(0); + expect(store.load).toBeCalledTimes(3); + }); + }); + + describe('when it is true', () => { + it.skip('should cache previously loaded pages', () => {}); + it.skip('should clear cache if not only pageIndex changed', () => {}); + }); + }); + + describe('dataSourse', () => { + describe('when it is dataSource instance', () => { + it('should pass dataSource as is', () => { + const dataSource = new DataSource({ + store: [{ a: 1 }, { b: 2 }], + }); + + const { dataController } = setup({ dataSource }); + + expect(dataController.dataSource.unreactive_get()).toBe(dataSource); + }); + }); + describe('when it is array', () => { + it('should normalize to DataSource with given items', () => { + const data = [{ a: 1 }, { b: 2 }]; + const { dataController } = setup({ dataSource: data }); + + const dataSource = dataController.dataSource.unreactive_get(); + + expect(dataSource).toBeInstanceOf(DataSource); + expect(dataSource.items()).toEqual(data); + }); + }); + describe('when it is empty', () => { + it('should should normalize to empty DataSource', () => { + const { dataController } = setup({}); + + const dataSource = dataController.dataSource.unreactive_get(); + + expect(dataSource).toBeInstanceOf(DataSource); + expect(dataSource.items()).toHaveLength(0); + }); + }); + }); + + describe('keyExpr', () => { + describe('when dataSource is array', () => { + it('should be passed as key to DataSource', () => { + const { dataController } = setup({ + dataSource: [{ myKeyExpr: 1 }, { myKeyExpr: 2 }], + keyExpr: 'myKeyExpr', + }); + + const dataSource = dataController.dataSource.unreactive_get(); + expect(dataSource.key()).toBe('myKeyExpr'); + }); + }); + describe('when dataSource is DataSource instance', () => { + it('should be ignored', () => { + const { dataController } = setup({ + dataSource: new ArrayStore({ + key: 'storeKeyExpr', + data: [{ storeKeyExpr: 1 }, { storeKeyExpr: 2 }], + }), + keyExpr: 'myKeyExpr', + }); + + const dataSource = dataController.dataSource.unreactive_get(); + expect(dataSource.key()).toBe('storeKeyExpr'); + }); + }); + }); + + describe('onDataErrorOccurred', () => { + it('should be called when load error happens', async () => { + const onDataErrorOccurred = jest.fn(); + + const { dataController } = setup({ + dataSource: new CustomStore({ + load() { + return Promise.reject(new Error('my error')); + }, + }), + onDataErrorOccurred, + }); + + await dataController.waitLoaded(); + + expect(onDataErrorOccurred).toBeCalledTimes(1); + expect(onDataErrorOccurred.mock.calls[0]).toMatchSnapshot([{ + component: expect.any(Object), + }]); + }); + }); + + describe('paging.enabled', () => { + describe('when it is true', () => { + it('should turn on pagination', () => { + const { dataController } = setup({ + dataSource: [{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }], + paging: { + enabled: true, + pageSize: 2, + }, + }); + + const items = dataController.items.unreactive_get(); + expect(items).toHaveLength(2); + }); + }); + describe('when it is false', () => { + it('should turn on pagination', () => { + const { dataController } = setup({ + dataSource: [{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }], + paging: { + enabled: false, + pageSize: 2, + }, + }); + + const items = dataController.items.unreactive_get(); + expect(items).toHaveLength(4); + }); + }); + }); + + describe('paging.pageIndex', () => { + it('should change current page', () => { + const { dataController, optionsController } = setup({ + dataSource: [{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }], + paging: { + pageSize: 2, + pageIndex: 1, + }, + }); + + let items = dataController.items.unreactive_get(); + expect(items).toEqual([{ a: '3' }, { a: '4' }]); + + optionsController.option('paging.pageIndex', 0); + items = dataController.items.unreactive_get(); + expect(items).toEqual([{ a: '1' }, { a: '2' }]); + }); + }); + + describe('paging.pageSize', () => { + it('should change size of current page', () => { + const { dataController, optionsController } = setup({ + dataSource: [{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }], + paging: { + pageSize: 2, + }, + }); + + let items = dataController.items.unreactive_get(); + expect(items).toEqual([{ a: '1' }, { a: '2' }]); + + optionsController.option('paging.pageSize', 3); + items = dataController.items.unreactive_get(); + expect(items).toEqual([{ a: '1' }, { a: '2' }, { a: '3' }]); + }); + }); + + describe.skip('remoteOperations', () => { + + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.ts new file mode 100644 index 000000000000..0667209bf3d6 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/options.ts @@ -0,0 +1,40 @@ +import type { DataSourceLike } from '@js/data/data_source'; + +import type { Action } from '../types'; + +interface PagingOptions { + enabled?: boolean; + pageSize?: number; + pageIndex?: number; +} + +interface RemoteOperationsOptions { + filtering?: boolean; + paging?: boolean; + sorting?: boolean; + summary?: boolean; +} + +export interface Options { + cacheEnabled?: boolean; + dataSource?: DataSourceLike; + keyExpr?: string | string[]; + onDataErrorOccurred?: Action<{ error: string }>; + paging?: PagingOptions; + remoteOperations?: RemoteOperationsOptions | boolean; +} + +export const defaultOptions = { + paging: { + enabled: true, + pageSize: 6, + pageIndex: 0, + }, + remoteOperations: { + filtering: false, + paging: false, + sorting: false, + summary: false, + }, + cacheEnabled: true, +} satisfies Options; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.test.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.test.ts new file mode 100644 index 000000000000..c0adb8e137e7 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.test.ts @@ -0,0 +1,171 @@ +/* eslint-disable spellcheck/spell-checker */ +import { + describe, expect, it, jest, +} from '@jest/globals'; +import ArrayStore from '@ts/data/m_array_store'; + +import type { Options } from '../options'; +import { OptionsControllerMock } from '../options_controller/options_controller.mock'; +import { DataController } from './data_controller'; +import { PublicMethods } from './public_methods'; + +const setup = (options: Options) => { + const optionsController = new OptionsControllerMock(options); + const dataController = new DataController(optionsController); + // @ts-expect-error + const gridCore = new (PublicMethods(class { + protected dataController = dataController; + }))(); + + return { + optionsController, + dataController, + gridCore, + }; +}; + +describe('PublicMethods', () => { + describe('getDataSource', () => { + it('should return current dataSource', () => { + const data = [{ a: 1 }, { b: 2 }]; + const { gridCore, dataController } = setup({ dataSource: data }); + + expect( + gridCore.getDataSource(), + ).toBe( + dataController.dataSource.unreactive_get(), + ); + }); + }); + describe('byKey', () => { + it('should return item by key', async () => { + const { gridCore } = setup({ + keyExpr: 'id', + dataSource: [ + { id: 1, value: 'value 1' }, + { id: 2, value: 'value 2' }, + ], + }); + + expect(await gridCore.byKey(1)).toEqual({ id: 1, value: 'value 1' }); + expect(await gridCore.byKey(2)).toEqual({ id: 2, value: 'value 2' }); + }); + + describe('when needed item is already loaded', () => { + it('should return item by given key without request', async () => { + const store = new ArrayStore({ + data: [ + { id: 1, value: 'value 1' }, + { id: 2, value: 'value 2' }, + { id: 3, value: 'value 3' }, + ], + key: 'id', + }); + + jest.spyOn(store, 'byKey'); + + const { gridCore, dataController } = setup({ dataSource: store }); + await dataController.waitLoaded(); + + const item = await gridCore.byKey(1); + expect(store.byKey).toBeCalledTimes(0); + expect(item).toEqual({ id: 1, value: 'value 1' }); + }); + }); + describe('when needed item is not already loaded', () => { + it('should make request to get item by given key', async () => { + const store = new ArrayStore({ + data: [ + { id: 1, value: 'value 1' }, + { id: 2, value: 'value 2' }, + { id: 3, value: 'value 3' }, + ], + key: 'id', + }); + + jest.spyOn(store, 'byKey'); + + const { gridCore, dataController } = setup({ + dataSource: store, + paging: { pageSize: 1 }, + }); + await dataController.waitLoaded(); + + const item = await gridCore.byKey(2); + expect(store.byKey).toBeCalledTimes(1); + expect(item).toEqual({ id: 2, value: 'value 2' }); + }); + }); + }); + describe('getFilter', () => { + // TODO: add test once some filter module (header filter, filter row etc) is implemented + it.skip('should return filter applied to dataSource', () => { + }); + }); + + describe('keyOf', () => { + it('should return key of given data object', () => { + const { gridCore } = setup({ keyExpr: 'id', dataSource: [] }); + const dataObject = { value: 'my value', id: 'my id' }; + + expect(gridCore.keyOf(dataObject)).toBe('my id'); + }); + }); + + describe('pageCount', () => { + it('should return current page count', () => { + const { gridCore, dataController } = setup({ + dataSource: [{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }], + paging: { + pageSize: 2, + }, + }); + expect(gridCore.pageCount()).toBe(2); + + dataController.pageSize.update(4); + expect(gridCore.pageCount()).toBe(1); + }); + }); + + describe('pageSize', () => { + it('should return current page size', () => { + const { gridCore, dataController } = setup({ + dataSource: [{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }], + paging: { + pageSize: 2, + }, + }); + expect(gridCore.pageSize()).toBe(2); + + dataController.pageSize.update(4); + expect(gridCore.pageSize()).toBe(4); + }); + }); + + describe('pageIndex', () => { + it('should return current page index', () => { + const { gridCore, dataController } = setup({ + dataSource: [{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }], + paging: { + pageSize: 2, + }, + }); + expect(gridCore.pageIndex()).toBe(0); + + dataController.pageIndex.update(3); + expect(gridCore.pageIndex()).toBe(3); + }); + }); + + describe('totalCount', () => { + it('should return current total count', () => { + const { gridCore } = setup({ + dataSource: [{ a: '1' }, { a: '2' }, { a: '3' }, { a: '4' }], + paging: { + pageSize: 2, + }, + }); + expect(gridCore.totalCount()).toBe(4); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts new file mode 100644 index 000000000000..1a1ec73a2c19 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/public_methods.ts @@ -0,0 +1,71 @@ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/no-invalid-void-type */ +/* eslint-disable spellcheck/spell-checker */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import type { FilterDescriptor } from '@js/data'; +import type DataSource from '@js/data/data_source'; +import { keysEqual } from '@ts/data/m_utils'; + +import type { Constructor } from '../types'; +import type { GridCoreNewBase } from '../widget'; +import type { DataObject, Key } from './types'; + +export function PublicMethods>(GridCore: T) { + return class GridCoreWithDataController extends GridCore { + public getDataSource(): DataSource { + return this.dataController.dataSource.unreactive_get(); + } + + public byKey(key: Key): Promise | undefined { + const items = this.getDataSource().items(); + const store = this.getDataSource().store(); + const keyExpr = store.key(); + + const foundItem = items.find( + (item) => keysEqual(keyExpr, key, this.keyOf(item)), + ); + + if (foundItem) { + return Promise.resolve(foundItem); + } + + return store.byKey(key); + } + + public getFilter(): FilterDescriptor | FilterDescriptor[] { + return this.getDataSource().filter(); + } + + public keyOf(obj: DataObject) { + return this.dataController.getDataKey(obj); + } + + public pageCount(): number { + return this.dataController.pageCount.unreactive_get(); + } + + public pageSize(): number; + public pageSize(value: number): void; + public pageSize(value?: number): number | void { + if (value === undefined) { + return this.dataController.pageSize.unreactive_get(); + } + this.dataController.pageSize.update(value); + } + + public pageIndex(): number; + public pageIndex(newIndex: number): void; + public pageIndex(newIndex?: number): number | void { + if (newIndex === undefined) { + return this.dataController.pageIndex.unreactive_get(); + } + // TODO: Promise (jQuery or native) + return this.dataController.pageIndex.update(newIndex); + } + + public totalCount(): number { + return this.dataController.totalCount.unreactive_get(); + } + }; +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/types.ts new file mode 100644 index 000000000000..ceb8279bec89 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/types.ts @@ -0,0 +1,3 @@ +export type DataObject = Record; +export type Key = unknown; +export type KeyExpr = unknown; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts new file mode 100644 index 000000000000..4fa3c0239553 --- /dev/null +++ b/packages/devextreme/js/__internal/grids/new/grid_core/data_controller/utils.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import type { DataSourceLike } from '@js/data/data_source'; +import DataSource from '@js/data/data_source'; +import { normalizeDataSourceOptions } from '@js/data/data_source/utils'; +import { applyBatch } from '@ts/data/m_array_utils'; + +import type { DataObject } from './types'; + +export function normalizeDataSource( + dataSourceLike: DataSourceLike | null | undefined, + keyExpr: string | string[] | undefined, +): DataSource { + if (dataSourceLike instanceof DataSource) { + return dataSourceLike; + } + + if (Array.isArray(dataSourceLike)) { + // eslint-disable-next-line no-param-reassign + dataSourceLike = { + store: { + type: 'array', + data: dataSourceLike, + key: keyExpr, + }, + }; + } + + // TODO: research making second param not required + return new DataSource(normalizeDataSourceOptions(dataSourceLike, undefined)); +} + +export function updateItemsImmutable( + data: DataObject[], + changes: any[], + keyInfo: any, +): DataObject[] { + // @ts-expect-error + return applyBatch({ + keyInfo, + data, + changes, + immutable: true, + }); +} diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts index 988fea008c30..b3fbbf69a803 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options.ts @@ -3,6 +3,7 @@ import { isMaterialBased } from '@js/ui/themes'; import type { WidgetOptions } from '@js/ui/widget/ui.widget'; import * as columnsController from './columns_controller/index'; +import * as dataController from './data_controller/index'; import type { GridCoreNew } from './widget'; /** @@ -10,9 +11,11 @@ import type { GridCoreNew } from './widget'; */ export type Options = & WidgetOptions + & dataController.Options & columnsController.Options; export const defaultOptions = { + ...dataController.defaultOptions, ...columnsController.defaultOptions, } satisfies Options; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.mock.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.mock.ts index 3a3a1747e202..7cb01f9e1f9c 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.mock.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.mock.ts @@ -20,6 +20,7 @@ export class OptionsControllerMock< this.componentMock = componentMock; } + // TODO: add typing public option(key?: string, value?: unknown): unknown { // @ts-expect-error return this.componentMock.option(key, value); diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts index 8bc4c67b7958..98a2b5424748 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/options_controller/options_controller_base.ts @@ -11,7 +11,7 @@ import { computed, state } from '@ts/core/reactive/index'; import type { ComponentType } from 'inferno'; import { TemplateWrapper } from '../inferno_wrappers/template_wrapper'; -import type { Template } from '../types'; +import type { Action, Template } from '../types'; type OwnProperty = TPropName extends keyof Required @@ -38,6 +38,11 @@ type TemplateProperty = ? ComponentType | undefined : unknown; +type ActionProperty = + NonNullable> extends Action + ? (args: TActionArgs) => void + : unknown; + function cloneObjectValue | unknown[]>( value: T, ): T { @@ -83,7 +88,7 @@ export class OptionsController { ) { this.props = state(component.option()); // @ts-expect-error - this.defaults = component._getDefaultOptions(); + this.defaults = component._getDefaultOptions?.() ?? {}; this.updateIsControlledMode(); component.on('optionChanged', (e: ChangedOptionInfo) => { @@ -161,7 +166,7 @@ export class OptionsController { public action( name: TProp, - ): SubsGets> { + ): SubsGets> { return computed( // @ts-expect-error () => this.component._createActionByOption(name) as any, diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/types.ts b/packages/devextreme/js/__internal/grids/new/grid_core/types.ts index 6549371773f2..0b91c9e01175 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/types.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/types.ts @@ -1,9 +1,16 @@ import type { template } from '@js/core/templates/template'; +import type { EventInfo } from '@js/events'; + +import type { GridCoreNew } from './widget'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type Constructor = new(...deps: TDeps) => T; // TODO -export type Template = (props: T) => HTMLDivElement | template; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export type Template = (props: TProps) => HTMLDivElement | template; + +// TODO: add TComponent +export type Action = (args: TArgs & EventInfo) => void; export type WithRequired = Omit & Required>; diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts index 280e3cc84294..0ad060d8e9b4 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/widget.ts @@ -10,6 +10,7 @@ import type { Subscription } from '@ts/core/reactive/index'; import { render } from 'inferno'; import * as ColumnsControllerModule from './columns_controller/index'; +import * as DataControllerModule from './data_controller/index'; import { MainView } from './main_view'; import { defaultOptions, defaultOptionsRules, type Options } from './options'; @@ -20,14 +21,18 @@ export class GridCoreNewBase< protected diContext!: DIContext; + protected dataController!: DataControllerModule.DataController; + protected columnsController!: ColumnsControllerModule.ColumnsController; protected _registerDIContext(): void { this.diContext = new DIContext(); + this.diContext.register(DataControllerModule.DataController); this.diContext.register(ColumnsControllerModule.ColumnsController); } protected _initDIContext(): void { + this.dataController = this.diContext.get(DataControllerModule.DataController); this.columnsController = this.diContext.get(ColumnsControllerModule.ColumnsController); } @@ -71,5 +76,7 @@ export class GridCoreNewBase< } export class GridCoreNew extends ColumnsControllerModule.PublicMethods( - GridCoreNewBase, + DataControllerModule.PublicMethods( + GridCoreNewBase, + ), ) {}