diff --git a/package-lock.json b/package-lock.json
index dd48a4d6229c..d35922f9c8c9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -46728,8 +46728,8 @@
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.0.1",
"@types/jest": "^26.0.24",
- "@types/react": "^18.2.20",
- "@types/react-dom": "^18.2.7",
+ "@types/react": "~18.0.0",
+ "@types/react-dom": "~18.0.0",
"del": "^3.0.0",
"gulp": "^4.0.2",
"gulp-header": "^2.0.9",
@@ -46810,9 +46810,9 @@
}
},
"packages/devextreme-react/node_modules/@types/react": {
- "version": "18.2.20",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.20.tgz",
- "integrity": "sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==",
+ "version": "18.0.38",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.38.tgz",
+ "integrity": "sha512-ExsidLLSzYj4cvaQjGnQCk4HFfVT9+EZ9XZsQ8Hsrcn8QNgXtpZ3m9vSIC2MWtx7jHictK6wYhQgGh6ic58oOw==",
"dev": true,
"dependencies": {
"@types/prop-types": "*",
@@ -46820,6 +46820,15 @@
"csstype": "^3.0.2"
}
},
+ "packages/devextreme-react/node_modules/@types/react-dom": {
+ "version": "18.0.11",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.11.tgz",
+ "integrity": "sha512-O38bPbI2CWtgw/OoQoY+BRelw7uysmXbWvw3nLWO21H1HSh+GOlqPuXshJfjmpNlKiiSDG9cc1JZAaMmVdcTlw==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"packages/devextreme-react/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
diff --git a/packages/devextreme-react/.eslintrc b/packages/devextreme-react/.eslintrc
index 32c54521a169..75bafa3c385d 100644
--- a/packages/devextreme-react/.eslintrc
+++ b/packages/devextreme-react/.eslintrc
@@ -43,7 +43,7 @@
},
"overrides": [
{
- "files": [ "*.ts" ],
+ "files": [ "*.ts", "*.tsx" ],
"rules": {
"import/extensions": "warn",
"max-len": ["error", { "code": 150 }],
diff --git a/packages/devextreme-react/jest.config.js b/packages/devextreme-react/jest.config.js
index d7edc0fced3e..ce6b489b2981 100644
--- a/packages/devextreme-react/jest.config.js
+++ b/packages/devextreme-react/jest.config.js
@@ -7,4 +7,10 @@ module.exports = {
...base,
name: packageName,
displayName: packageName,
+ testPathIgnorePatterns: [
+ "component.test.tsx",
+ "nested-option.test.tsx",
+ "props-updating.test.tsx",
+ "template.test.tsx",
+ ],
};
diff --git a/packages/devextreme-react/package.json b/packages/devextreme-react/package.json
index e00e09aa73c5..04bab8a4bba3 100644
--- a/packages/devextreme-react/package.json
+++ b/packages/devextreme-react/package.json
@@ -35,8 +35,8 @@
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.0.1",
"@types/jest": "^26.0.24",
- "@types/react": "^18.2.20",
- "@types/react-dom": "^18.2.7",
+ "@types/react": "~18.0.0",
+ "@types/react-dom": "~18.0.0",
"del": "^3.0.0",
"gulp": "^4.0.2",
"gulp-header": "^2.0.9",
diff --git a/packages/devextreme-react/src/core/__tests__/component.test.tsx b/packages/devextreme-react/src/core/__tests__/component.test.tsx
index 515fee665ee0..92b846ccd93f 100644
--- a/packages/devextreme-react/src/core/__tests__/component.test.tsx
+++ b/packages/devextreme-react/src/core/__tests__/component.test.tsx
@@ -10,6 +10,8 @@ import {
Widget,
WidgetClass,
} from './test-component';
+
+// @ts-ignore: Non-existent module
import { TemplatesRenderer } from '../templates-renderer';
jest.useFakeTimers();
@@ -160,7 +162,6 @@ describe('rendering', () => {
,
);
- // @ts-ignore
expect(WidgetClass.mock.calls[0][1]).toEqual({ templatesRenderAsynchronously: true });
});
@@ -193,7 +194,6 @@ describe('rendering', () => {
,
);
- // @ts-ignore
expect(WidgetClass.mock.calls[1][1].children).toBeUndefined();
});
});
diff --git a/packages/devextreme-react/src/core/__tests__/config.test.tsx b/packages/devextreme-react/src/core/__tests__/config.test.tsx
index f30f8d6bbc64..5529ebb5057d 100644
--- a/packages/devextreme-react/src/core/__tests__/config.test.tsx
+++ b/packages/devextreme-react/src/core/__tests__/config.test.tsx
@@ -50,8 +50,7 @@ describe('useLegacyTemplateEngine', () => {
(() => Widget);
class TestExtensionComponent extends ExtensionComponent
{
constructor(props: P) {
- super(props);
+ super(props as P & IHtmlOptions);
this._WidgetClass = ExtensionWidgetClass;
}
@@ -57,7 +58,6 @@ it('creates widget on componentDidMount inside another component on same element
);
expect(ExtensionWidgetClass).toHaveBeenCalledTimes(1);
- // @ts-ignore
expect(ExtensionWidgetClass.mock.calls[0][0]).toBe(WidgetClass.mock.calls[0][0]);
});
@@ -78,11 +78,9 @@ it('pulls options from a single nested component', () => {
,
);
- // @ts-ignore
const options = ExtensionWidgetClass.mock.calls[0][1];
expect(options).toHaveProperty('option1');
- // @ts-ignore
expect(options.option1).toMatchObject({
a: 123,
});
diff --git a/packages/devextreme-react/src/core/__tests__/nested-option.test.tsx b/packages/devextreme-react/src/core/__tests__/nested-option.test.tsx
index 52375d9b2d87..6eeee4ed8088 100644
--- a/packages/devextreme-react/src/core/__tests__/nested-option.test.tsx
+++ b/packages/devextreme-react/src/core/__tests__/nested-option.test.tsx
@@ -1,18 +1,17 @@
-// @ts-nocheck
/* eslint-disable max-classes-per-file */
import { render, cleanup } from '@testing-library/react';
import * as React from 'react';
-import { Component } from '../component';
+import { Component, IHtmlOptions } from '../component';
import ConfigurationComponent from '../nested-option';
import { TestComponent, Widget, WidgetClass } from './test-component';
jest.useFakeTimers();
-class NestedComponent extends ConfigurationComponent<{ a: number }> {
+class NestedComponent extends ConfigurationComponent<{ a: number } & React.PropsWithChildren> {
public static OptionName = 'option';
}
-class NestedComponentWithPredfeinedProps extends ConfigurationComponent<{ a: number }> {
+class NestedComponentWithPredfeinedProps extends ConfigurationComponent<{ a: number } & React.PropsWithChildren> {
public static OptionName = 'option';
public static PredefinedProps = {
@@ -170,7 +169,7 @@ describe('nested option', () => {
});
it('is pulled according to expectations', () => {
- class TestComponentWithExpectation
extends Component
{
+ class TestComponentWithExpectation
extends Component
{
protected _expectedChildren = {
option: {
optionName: 'expectedItemOptions',
@@ -505,7 +504,7 @@ describe('nested sub-option', () => {
});
it('is pulled according to expectations', () => {
- class NestedComponentWithExpectations extends ConfigurationComponent<{ a: number }> {
+ class NestedComponentWithExpectations extends ConfigurationComponent<{ a: number } & React.PropsWithChildren> {
public static OptionName = 'option';
public static ExpectedChildren = {
diff --git a/packages/devextreme-react/src/core/__tests__/props-updating.test.tsx b/packages/devextreme-react/src/core/__tests__/props-updating.test.tsx
index bf4e39143d48..bfcbc297e2c1 100644
--- a/packages/devextreme-react/src/core/__tests__/props-updating.test.tsx
+++ b/packages/devextreme-react/src/core/__tests__/props-updating.test.tsx
@@ -1,4 +1,3 @@
-// @ts-nocheck
/* eslint-disable max-classes-per-file */
import { cleanup, render } from '@testing-library/react';
import * as React from 'react';
@@ -13,7 +12,10 @@ import {
Widget,
WidgetClass,
} from './test-component';
+
+// @ts-ignore: Non-existent module
import TemplatesManager from '../templates-manager';
+// @ts-ignore: Non-existent module
import { TemplatesStore } from '../templates-store';
jest.useFakeTimers();
@@ -32,7 +34,7 @@ interface IControlledComponentProps {
complexOption?: Record;
}
-class ControlledComponent extends TestComponent {
+class ControlledComponent extends TestComponent {
protected _defaults = {
defaultControlledOption: 'controlledOption',
};
@@ -47,7 +49,7 @@ class NestedComponent extends ConfigurationComponent<{
complexValue?: Record;
value?: number;
onValueChange?: (value: number) => void;
-}> {
+} & React.PropsWithChildren> {
public static DefaultsProps = {
defaultC: 'c',
};
@@ -58,7 +60,7 @@ class NestedComponent extends ConfigurationComponent<{
class CollectionNestedComponent extends ConfigurationComponent<{
a?: number;
onAChange?: (value: number) => void;
-}> {}
+} & React.PropsWithChildren> {}
(CollectionNestedComponent as any).OptionName = 'items';
(CollectionNestedComponent as any).IsCollectionItem = true;
(CollectionNestedComponent as any).ExpectedChildren = {
@@ -623,9 +625,7 @@ describe('cfg-component option control', () => {
// T1106899
it('apply cfg-component option value if value has changes', () => {
- const optionsManager = new OptionsManagerModule.OptionsManager(
- new TemplatesManager(new TemplatesStore(() => {})),
- );
+ const optionsManager = new OptionsManagerModule.OptionsManager();
const config = {
fullName: '',
predefinedOptions: {},
@@ -678,7 +678,7 @@ describe('cfg-component option control', () => {
expect(OptionsManagerModule.scheduleGuards).toBeCalled();
const updatedConfig = { ...config, options: { value: 2 } };
// value changed and options manager set value and remove scheduled guard
- optionsManager.update(updatedConfig);
+ optionsManager.update(updatedConfig, {});
expect((optionsManager as any).setValue).toBeCalled();
jest.runAllTimers();
expect((optionsManager as any).setValue).toHaveBeenCalledTimes(1);
@@ -896,7 +896,7 @@ describe('onXXXChange', () => {
});
beforeAll(() => {
- jest.spyOn(
+ jest.spyOn<{ isOptionSubscribable: () => boolean }, 'isOptionSubscribable'>(
OptionsManagerModule.OptionsManager.prototype as
OptionsManagerModule.OptionsManager & { isOptionSubscribable: () => boolean },
'isOptionSubscribable',
@@ -1132,7 +1132,7 @@ describe('onXXXChange', () => {
describe('non-subscribable options', () => {
beforeAll(() => {
- jest.spyOn(
+ jest.spyOn<{ isOptionSubscribable: () => boolean }, 'isOptionSubscribable'>(
OptionsManagerModule.OptionsManager.prototype as
OptionsManagerModule.OptionsManager & { isOptionSubscribable: () => boolean },
'isOptionSubscribable',
@@ -1173,7 +1173,7 @@ describe('onXXXChange', () => {
describe('independent events', () => {
beforeAll(() => {
- jest.spyOn(
+ jest.spyOn<{ isIndependentEvent: () => boolean }, 'isIndependentEvent'>(
OptionsManagerModule.OptionsManager.prototype as
OptionsManagerModule.OptionsManager & { isIndependentEvent: () => boolean },
'isIndependentEvent',
@@ -1230,7 +1230,7 @@ describe('onXXXChange', () => {
describe('dependent events', () => {
beforeAll(() => {
- jest.spyOn(
+ jest.spyOn<{ isIndependentEvent: () => boolean }, 'isIndependentEvent'>(
OptionsManagerModule.OptionsManager.prototype as
OptionsManagerModule.OptionsManager & { isIndependentEvent: () => boolean },
'isIndependentEvent',
diff --git a/packages/devextreme-react/src/core/__tests__/template.test.tsx b/packages/devextreme-react/src/core/__tests__/template.test.tsx
index a46140abb3df..67bba55c8d66 100644
--- a/packages/devextreme-react/src/core/__tests__/template.test.tsx
+++ b/packages/devextreme-react/src/core/__tests__/template.test.tsx
@@ -1,10 +1,9 @@
-// @ts-nocheck
/* eslint-disable max-classes-per-file */
import * as events from 'devextreme/events';
import { cleanup, render, screen } from '@testing-library/react';
import * as React from 'react';
import { act } from 'react-dom/test-utils';
-import { Component } from '../component';
+import { Component, IHtmlOptions } from '../component';
import ConfigurationComponent from '../nested-option';
import { Template } from '../template';
import {
@@ -12,7 +11,10 @@ import {
Widget,
WidgetClass,
} from './test-component';
+
+// @ts-ignore: Non-existent module
import { TemplatesStore } from '../templates-store';
+// @ts-ignore: Non-existent module
import { TemplateWrapperRenderer } from '../template-wrapper';
jest.useFakeTimers();
@@ -30,7 +32,7 @@ class ComponentWithTemplates extends TestComponent {
protected _templateProps = templateProps;
}
-class ComponentWithAsyncTemplates extends Component
{
+class ComponentWithAsyncTemplates
extends Component
{
protected _WidgetClass = WidgetClass;
protected _templateProps = templateProps;
@@ -326,7 +328,7 @@ function testTemplateOption(testedOption: string) {
,
);
- const componentInstance = ref.current as {
+ const componentInstance = ref.current as unknown as {
_templatesStore: { _templates: Record }; };
act(() => { renderItemTemplate({ text: 1 }); });
@@ -351,7 +353,7 @@ function testTemplateOption(testedOption: string) {
,
);
- const componentInstance = ref.current as {
+ const componentInstance = ref.current as unknown as {
_templatesStore: { _templates: Record }; };
const container = document.createElement('div');
@@ -370,7 +372,7 @@ function testTemplateOption(testedOption: string) {
elementOptions[testedOption] = prepareTemplate((data: Record) => (
Template
- {data.text}
+ {data.text as string}
));
elementOptions.itemKeyFn = (data) => data.text;
@@ -381,7 +383,7 @@ function testTemplateOption(testedOption: string) {
act(() => { renderItemTemplate({ text: 1 }); });
act(() => { renderItemTemplate({ text: 2 }); });
- const componentInstance = ref.current as { _templatesStore: { _templates: Record } };
+ const componentInstance = ref.current as unknown as { _templatesStore: { _templates: Record } };
const templatesKeys = Object.getOwnPropertyNames(componentInstance._templatesStore._templates);
expect(templatesKeys.length).toBe(2);
@@ -406,7 +408,7 @@ function testTemplateOption(testedOption: string) {
,
);
- const componentInstance = ref.current as { _templatesStore: TemplatesStore };
+ const componentInstance = ref.current as unknown as { _templatesStore: TemplatesStore };
act(() => { renderItemTemplate({}, refContainer.current); });
expect(componentInstance._templatesStore.renderWrappers().length).toBe(1);
@@ -450,7 +452,7 @@ function testTemplateOption(testedOption: string) {
,
);
- const componentInstance = ref.current as { _templatesStore: TemplatesStore };
+ const componentInstance = ref.current as unknown as { _templatesStore: TemplatesStore };
act(() => { renderItemTemplate(undefined, refContainer.current); });
expect(componentInstance._templatesStore.renderWrappers().length).toBe(1);
@@ -698,7 +700,7 @@ describe('component/render in nested options', () => {
item?: any;
itemRender?: any;
itemComponent?: any;
- }> {
+ } & React.PropsWithChildren> {
public static OptionName = 'option';
public static TemplateProps = [{
@@ -712,7 +714,7 @@ describe('component/render in nested options', () => {
template?: any;
render?: any;
component?: any;
- }> {
+ } & React.PropsWithChildren> {
public static IsCollectionItem = true;
public static OptionName = 'collection';
diff --git a/packages/devextreme-react/src/core/__tests__/templates-renderer.test.tsx b/packages/devextreme-react/src/core/__tests__/templates-renderer.test.tsx
deleted file mode 100644
index fd4a558984de..000000000000
--- a/packages/devextreme-react/src/core/__tests__/templates-renderer.test.tsx
+++ /dev/null
@@ -1,118 +0,0 @@
-/* eslint-disable max-classes-per-file */
-import { requestAnimationFrame } from 'devextreme/animation/frame';
-import { deferUpdate } from 'devextreme/core/utils/common';
-import { render } from '@testing-library/react';
-import * as React from 'react';
-import { act } from 'react-dom/test-utils';
-import { TemplatesRenderer } from '../templates-renderer';
-import { TemplatesStore } from '../templates-store';
-
-const defaultWarn = global.console.warn;
-const defaultError = global.console.error;
-
-global.console.warn = (message) => {
- throw message;
-};
-
-global.console.error = (message) => {
- throw message;
-};
-
-jest.mock('devextreme/animation/frame', () => ({
- requestAnimationFrame: jest.fn(),
-}));
-
-jest.mock('devextreme/core/utils/common', () => ({
- deferUpdate: jest.fn(),
-}));
-[true, false].forEach((useDeferUpdate) => {
- describe(`useDeferUpdate === ${useDeferUpdate}`, () => {
- const updateFunctionMock = useDeferUpdate
- ? deferUpdate as jest.Mock
- : requestAnimationFrame as jest.Mock;
- let updateCallback;
-
- beforeEach(() => {
- updateFunctionMock.mockImplementation((func) => {
- updateCallback = func;
- });
- });
-
- afterEach(() => {
- jest.clearAllMocks();
- });
-
- afterAll(() => {
- global.console.warn = defaultWarn;
- global.console.error = defaultError;
- });
-
- it('should not throw warning when unmounted', async () => {
- const ref = React.createRef();
- const templatesStore = new TemplatesStore(() => { });
-
- const { unmount } = render();
-
- expect(ref.current).not.toBeNull();
-
- expect(() => ref.current?.scheduleUpdate(useDeferUpdate)).not.toThrow();
- expect(act(() => updateCallback())).resolves.not.toThrow();
-
- unmount();
- expect(ref.current).toBeNull();
-
- expect(() => updateCallback()).not.toThrow();
- });
-
- it(`should call ${useDeferUpdate ? 'deferUpdate' : 'requestAnimationFrame'}`, async () => {
- const ref = React.createRef();
- const templatesStore = new TemplatesStore(() => { });
-
- render();
- expect(() => ref.current?.scheduleUpdate(useDeferUpdate)).not.toThrow();
- expect(updateFunctionMock).toHaveBeenCalledTimes(1);
- });
-
- it('should not call twice', async () => {
- const ref = React.createRef();
- const templatesStore = new TemplatesStore(() => { });
-
- render();
- ref.current?.scheduleUpdate(useDeferUpdate);
- if (useDeferUpdate) {
- expect(deferUpdate).toHaveBeenCalledTimes(1);
- } else {
- expect(requestAnimationFrame).toHaveBeenCalledTimes(1);
- }
-
- ref.current?.scheduleUpdate(useDeferUpdate);
- expect(updateFunctionMock).toHaveBeenCalledTimes(1);
- });
- });
-});
-
-describe('option update', () => {
- it('should call forceUpdateCallback and reset "updateScheduled" after forceUpdateCallback', async () => {
- const ref = React.createRef();
- const templatesStore = new TemplatesStore(() => { });
-
- (deferUpdate as jest.Mock).mockImplementation((func, _) => {
- func();
- });
-
- render();
- const current = ref.current!;
- const onRendered = jest.fn();
- const spyForceUpdateCallback = jest.spyOn(current, 'forceUpdate').mockImplementation((cb) => {
- expect((current as any).updateScheduled).toEqual(true);
- expect(onRendered).not.toHaveBeenCalled();
- // @ts-ignore
- cb();
- expect(onRendered).toHaveBeenCalled();
-
- expect((current as any).updateScheduled).toEqual(false);
- });
- current.scheduleUpdate(true, onRendered);
- expect(spyForceUpdateCallback).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/packages/devextreme-react/src/core/__tests__/templates-store.test.tsx b/packages/devextreme-react/src/core/__tests__/templates-store.test.tsx
deleted file mode 100644
index 53fd530795a2..000000000000
--- a/packages/devextreme-react/src/core/__tests__/templates-store.test.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import { TemplatesStore } from '../templates-store';
-
-describe('templates-store', () => {
- it('adds', () => {
- const templatesStore = new TemplatesStore(jest.fn());
-
- templatesStore.add('1', jest.fn());
- templatesStore.add('2', jest.fn());
-
- expect(templatesStore.renderWrappers().length).toBe(2);
- });
-
- it('replaces items with equal identifiers', () => {
- const templatesStore = new TemplatesStore(jest.fn());
-
- templatesStore.add('1', jest.fn());
- templatesStore.add('1', jest.fn());
-
- expect(templatesStore.renderWrappers().length).toBe(1);
- });
-
- it('executes callback on item added', () => {
- const callback = jest.fn();
- const templatesStore = new TemplatesStore(callback);
-
- templatesStore.add('1', jest.fn());
-
- expect(callback.mock.calls.length).toBe(1);
-
- templatesStore.add('2', jest.fn());
-
- expect(callback.mock.calls.length).toBe(2);
- });
-
- it('setDeferredRemove for remove', () => {
- const templatesStore = new TemplatesStore(jest.fn());
-
- templatesStore.add('1', jest.fn());
- templatesStore.add('2', jest.fn());
-
- templatesStore.setDeferredRemove('1', true);
- expect(templatesStore.renderWrappers().length).toBe(1);
-
- templatesStore.setDeferredRemove('2', true);
- expect(templatesStore.renderWrappers().length).toBe(0);
- });
-
- it('setDeferredRemove not existing template correctly', () => {
- const templatesStore = new TemplatesStore(jest.fn());
-
- templatesStore.add('1', jest.fn());
-
- templatesStore.setDeferredRemove('2', true);
- templatesStore.setDeferredRemove('3', true);
- expect(templatesStore.renderWrappers().length).toBe(1);
- });
-
- it('does not execute callback on item removed', () => {
- const callback = jest.fn();
- const templatesStore = new TemplatesStore(callback);
-
- templatesStore.add('1', jest.fn());
- templatesStore.add('2', jest.fn());
-
- expect(callback.mock.calls.length).toBe(2);
-
- templatesStore.setDeferredRemove('1', true);
- templatesStore.setDeferredRemove('2', true);
-
- expect(callback.mock.calls.length).toBe(2);
- });
-
- it('toggle deferred remove shouldnt remove template (Unmount, Mount scenarion)', () => {
- const templatesStore = new TemplatesStore(jest.fn());
-
- templatesStore.add('1', jest.fn());
-
- templatesStore.setDeferredRemove('1', true);
- templatesStore.setDeferredRemove('1', false);
- expect(templatesStore.renderWrappers().length).toBe(1);
- });
-
- it('lists renderers execution results', () => {
- const templatesStore = new TemplatesStore(jest.fn());
- const renderer1 = jest.fn(() => 1);
- const renderer2 = jest.fn(() => 2);
-
- // @ts-ignore
- templatesStore.add('1', renderer1);
- // @ts-ignore
- templatesStore.add('2', renderer2);
-
- const wrappers = templatesStore.renderWrappers();
-
- expect(renderer1.mock.calls.length).toBe(1);
- expect(renderer2.mock.calls.length).toBe(1);
-
- expect(wrappers[0]).toBe(1);
- expect(wrappers[1]).toBe(2);
- });
-});
diff --git a/packages/devextreme-react/src/core/__tests__/test-component.ts b/packages/devextreme-react/src/core/__tests__/test-component.ts
index 136ee4d25d72..6d32b1d9e269 100644
--- a/packages/devextreme-react/src/core/__tests__/test-component.ts
+++ b/packages/devextreme-react/src/core/__tests__/test-component.ts
@@ -1,5 +1,5 @@
/* eslint-disable max-classes-per-file */
-import { Component } from '../component';
+import { Component, IHtmlOptions } from '../component';
import DOMComponent from 'devextreme/core/dom_component';
const eventHandlers: { [index: string]: ((e?: any) => void)[] } = {};
@@ -29,10 +29,9 @@ const Widget = {
skipOptionsRollBack: false,
};
-const WidgetClass = jest.fn(() => Widget);
+const WidgetClass = jest.fn(() => Widget);
-// @ts-expect-error
-class TestComponent extends Component
{
+class TestComponent
extends Component
{
protected _WidgetClass = WidgetClass;
protected useDeferUpdateFlag = true;
diff --git a/packages/devextreme-react/src/core/component-base.ts b/packages/devextreme-react/src/core/component-base.ts
index d8fe0dacc6e8..dfd17315b53a 100644
--- a/packages/devextreme-react/src/core/component-base.ts
+++ b/packages/devextreme-react/src/core/component-base.ts
@@ -2,11 +2,9 @@ import * as events from 'devextreme/events';
import * as React from 'react';
import { createPortal } from 'react-dom';
-import TemplatesManager from './templates-manager';
-import { TemplatesRenderer } from './templates-renderer';
-import { TemplatesStore } from './templates-store';
+import { TemplateManager } from './template-manager';
-import { OptionsManager, scheduleGuards, unscheduleGuards } from './options-manager';
+import { OptionsManager, unscheduleGuards } from './options-manager';
import { ITemplateMeta } from './template';
import { elementPropNames, getClassName } from './widget-config';
@@ -14,6 +12,8 @@ import { IConfigNode } from './configuration/config-node';
import { IExpectedChild } from './configuration/react/element';
import { buildConfigTree } from './configuration/react/tree';
import { isIE } from './configuration/utils';
+import { DXRemoveCustomArgs, DXTemplateCreator, UpdateLocker } from './types';
+import { RemovalLockerContext } from './helpers';
const DX_REMOVE_EVENT = 'dxremove';
@@ -35,6 +35,10 @@ abstract class ComponentBase
extends React.PureComponent
: { display: 'contents' };
}
+ static contextType: React.Context = RemovalLockerContext;
+
+ declare context: React.ContextType | undefined;
+
protected _WidgetClass: any;
protected _instance: any;
@@ -55,13 +59,11 @@ abstract class ComponentBase extends React.PureComponent
protected readonly independentEvents: string[];
- private _childNodes: Node[] = [];
-
- private _templatesRendererRef: TemplatesRenderer | null;
+ private _createDXTemplates: DXTemplateCreator | undefined;
- private readonly _templatesStore: TemplatesStore;
+ private _clearInstantiationModels: (() => void) | undefined;
- private readonly _templatesManager: TemplatesManager;
+ private _childNodes: Node[] = [];
private readonly _optionsManager: OptionsManager;
@@ -72,16 +74,10 @@ abstract class ComponentBase
extends React.PureComponent
constructor(props: P) {
super(props);
- this._setTemplatesRendererRef = this._setTemplatesRendererRef.bind(this);
this._createWidget = this._createWidget.bind(this);
+ this._setTemplateManagerHooks = this._setTemplateManagerHooks.bind(this);
- this._templatesStore = new TemplatesStore(() => {
- if (this._templatesRendererRef) {
- this._templatesRendererRef.scheduleUpdate(this.useDeferUpdateForTemplates);
- }
- });
- this._templatesManager = new TemplatesManager(this._templatesStore);
- this._optionsManager = new OptionsManager(this._templatesManager);
+ this._optionsManager = new OptionsManager();
}
public componentDidMount(): void {
@@ -101,34 +97,55 @@ abstract class ComponentBase
extends React.PureComponent
public componentDidUpdate(prevProps: P): void {
this._updateCssClasses(prevProps, this.props);
+
const config = this._getConfig();
- this._optionsManager.update(config);
- if (this._templatesRendererRef) {
- this._templatesRendererRef.scheduleUpdate(this.useDeferUpdateForTemplates, scheduleGuards);
- }
+ const templateOptions = this._optionsManager.getTemplateOptions(config);
+ const dxTemplates = this._createDXTemplates?.(templateOptions) || {};
+
+ this._optionsManager.update(config, dxTemplates);
unscheduleGuards();
}
public componentWillUnmount(): void {
+ this._lockParentOnRemoved();
+
if (this._instance) {
+ const dxRemoveArgs: DXRemoveCustomArgs = { isUnmounting: true };
+
this._childNodes?.forEach((child) => child.parentNode?.removeChild(child));
- events.triggerHandler(this._element, DX_REMOVE_EVENT);
+ events.triggerHandler(this._element, DX_REMOVE_EVENT, dxRemoveArgs);
this._instance.dispose();
}
this._optionsManager.dispose();
+
+ this._unlockParentOnRemoved();
}
protected _createWidget(element?: Element): void {
element = element || this._element;
const config = this._getConfig();
- this._instance = new this._WidgetClass(
- element,
- {
- templatesRenderAsynchronously: true,
- ...this._optionsManager.getInitialOptions(config),
- },
- );
+
+ let options: any = {
+ templatesRenderAsynchronously: true,
+ ...this._optionsManager.getInitialOptions(config),
+ };
+
+ const templateOptions = this._optionsManager.getTemplateOptions(config);
+ const dxTemplates = this._createDXTemplates?.(templateOptions);
+
+ if (dxTemplates) {
+ options = {
+ ...options,
+ integrationOptions: {
+ templates: dxTemplates,
+ },
+ };
+ }
+
+ this._clearInstantiationModels?.();
+
+ this._instance = new this._WidgetClass(element, options);
if (!this.useRequestAnimationFrameFlag) {
this.useDeferUpdateForTemplates = this._instance.option(
@@ -152,10 +169,6 @@ abstract class ComponentBase
extends React.PureComponent
);
}
- private _setTemplatesRendererRef(instance: TemplatesRenderer | null) {
- this._templatesRendererRef = instance;
- }
-
private _getElementProps(): Record {
const elementProps: Record = {
ref: (element: HTMLDivElement) => { this._element = element; },
@@ -199,6 +212,19 @@ abstract class ComponentBase extends React.PureComponent
}
}
+ private _lockParentOnRemoved() {
+ this.context?.lock();
+ }
+
+ private _unlockParentOnRemoved() {
+ this.context?.unlock();
+ }
+
+ private _setTemplateManagerHooks(createDXTemplates: DXTemplateCreator, clearInstantiationModels: () => void) {
+ this._createDXTemplates = createDXTemplates;
+ this._clearInstantiationModels = clearInstantiationModels;
+ }
+
protected renderChildren(): React.ReactNode {
// @ts-expect-error TS2339
const { children } = this.props;
@@ -223,7 +249,6 @@ abstract class ComponentBase
extends React.PureComponent
}
protected renderPortal(): React.ReactNode {
- // @ts-expect-error TS2322
return this.portalContainer && createPortal(
this.renderChildren(),
this.portalContainer,
@@ -238,9 +263,8 @@ abstract class ComponentBase
extends React.PureComponent
'div',
this._getElementProps(),
this.renderContent(),
- React.createElement(TemplatesRenderer, {
- templatesStore: this._templatesStore,
- ref: this._setTemplatesRendererRef,
+ React.createElement(TemplateManager, {
+ init: this._setTemplateManagerHooks,
}),
),
this.isPortalComponent && this.renderPortal(),
diff --git a/packages/devextreme-react/src/core/dx-template.ts b/packages/devextreme-react/src/core/dx-template.ts
deleted file mode 100644
index cb7cc28456c6..000000000000
--- a/packages/devextreme-react/src/core/dx-template.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import * as React from 'react';
-
-import * as events from 'devextreme/events';
-import { DoubleKeyMap, generateID } from './helpers';
-import { ITemplateArgs } from './template';
-import { ITemplateWrapperProps, TemplateWrapper } from './template-wrapper';
-import { TemplatesStore } from './templates-store';
-import { DX_REMOVE_EVENT } from './component-base';
-
-interface IDxTemplate {
- render: (data: IDxTemplateData) => any;
-}
-
-interface IDxTemplateData {
- container: any;
- model?: any;
- index?: any;
- onRendered?: () => void;
-}
-
-function unwrapElement(element: any): HTMLElement {
- return element.get ? element.get(0) : element;
-}
-
-function createDxTemplate(
- createContentProvider: () => (props: ITemplateArgs) => any,
- templatesStore: TemplatesStore,
- keyFn?: (data: any) => string,
-): IDxTemplate {
- const renderedTemplates = new DoubleKeyMap();
-
- return {
- render: (data: IDxTemplateData) => {
- const container = unwrapElement(data.container);
- const key = { key1: data.model, key2: container };
- const prevTemplateId = renderedTemplates.get(key);
-
- let templateId: string;
-
- const onRemoved = (): void => {
- templatesStore.setDeferredRemove(templateId, true);
- renderedTemplates.delete(key);
- };
-
- const _subscribeOnContainerRemoval = (): void => {
- if (container.nodeType === Node.ELEMENT_NODE) {
- events.one(container, DX_REMOVE_EVENT, onRemoved);
- }
- };
-
- const _unsubscribeOnContainerRemoval = (): void => {
- if (container.nodeType === Node.ELEMENT_NODE) {
- events.off(container, DX_REMOVE_EVENT, onRemoved);
- }
- };
-
- if (prevTemplateId) {
- templateId = prevTemplateId;
- } else {
- templateId = keyFn ? keyFn(data.model) : `__template_${generateID()}`;
-
- if (data.model !== undefined) {
- renderedTemplates.set(key, templateId);
- }
- }
-
- _subscribeOnContainerRemoval();
-
- templatesStore.add(templateId, () => {
- const props: ITemplateArgs = {
- data: data.model,
- index: data.index,
- };
-
- const contentProvider = createContentProvider();
- return React.createElement(
- TemplateWrapper,
- {
- content: contentProvider(props),
- container,
- onRemoved,
- onDidMount: () => {
- _unsubscribeOnContainerRemoval();
- templatesStore.setDeferredRemove(templateId, false);
- data.onRendered?.();
- },
- key: templateId,
- },
- ) as any as TemplateWrapper;
- });
-
- return container;
- },
- };
-}
-
-export {
- IDxTemplate,
- createDxTemplate,
-};
diff --git a/packages/devextreme-react/src/core/helpers.ts b/packages/devextreme-react/src/core/helpers.ts
index 3a0f485f155f..a65d741c38a3 100644
--- a/packages/devextreme-react/src/core/helpers.ts
+++ b/packages/devextreme-react/src/core/helpers.ts
@@ -1,9 +1,18 @@
+/* eslint-disable max-classes-per-file, no-restricted-syntax */
+import { createContext } from 'react';
+import { TemplateInstantiationModel, UpdateLocker } from './types';
+
+export const RemovalLockerContext = createContext({
+ lock: () => undefined,
+ unlock: () => undefined,
+});
+
export function generateID(): string {
- return Math.random().toString(36).substr(2);
+ return Math.random().toString(36).substring(2);
}
export class DoubleKeyMap {
- private readonly _map: Map> = new Map();
+ private _map: Map> = new Map();
public set({ key1, key2 }: { key1: TKey1; key2: TKey2 }, value: TValue): void {
let innerMap = this._map.get(key1);
@@ -31,8 +40,29 @@ export class DoubleKeyMap {
this._map.delete(key1);
}
}
+
+ public get empty(): boolean {
+ return this._map.size === 0;
+ }
+
+ public shallowCopy(): DoubleKeyMap {
+ const copy = new DoubleKeyMap();
+
+ copy._map = this._map;
+ return copy;
+ }
+
+ * [Symbol.iterator](): Generator<[{ key1: TKey1; key2: TKey2 }, TValue]> {
+ for (const [key1, innerMap] of this._map) {
+ for (const [key2, value] of innerMap) {
+ yield [{ key1, key2 }, value];
+ }
+ }
+ }
}
+export class TemplateInstantiationModels extends DoubleKeyMap {}
+
export function capitalizeFirstLetter(text: string): string {
if (text.length) {
return `${text[0].toUpperCase()}${text.substr(1)}`;
diff --git a/packages/devextreme-react/src/core/options-manager.ts b/packages/devextreme-react/src/core/options-manager.ts
index c129d71701be..ed714947ed19 100644
--- a/packages/devextreme-react/src/core/options-manager.ts
+++ b/packages/devextreme-react/src/core/options-manager.ts
@@ -3,9 +3,8 @@ import { getChanges } from './configuration/comparer';
import { buildConfig, findValue, ValueType } from './configuration/tree';
import { mergeNameParts, shallowEquals } from './configuration/utils';
import { capitalizeFirstLetter } from './helpers';
-
-import type TemplatesManager from './templates-manager';
-import type { IConfigNode } from './configuration/config-node';
+import type { IConfigNode, ITemplate } from './configuration/config-node';
+import { DXTemplateCollection } from './types';
const optionsManagers = new Set();
let guardTimeoutHandler = -1;
@@ -23,8 +22,6 @@ export function scheduleGuards(): void {
class OptionsManager {
private readonly guards: Record void> = {};
- private readonly templatesManager: TemplatesManager;
-
private instance: any;
private isUpdating = false;
@@ -35,8 +32,7 @@ class OptionsManager {
private independentEvents: Set;
- constructor(templatesManager: TemplatesManager) {
- this.templatesManager = templatesManager;
+ constructor() {
optionsManagers.add(this);
this.onOptionChanged = this.onOptionChanged.bind(this);
@@ -58,25 +54,22 @@ class OptionsManager {
public getInitialOptions(rootNode: IConfigNode): Record {
const config = buildConfig(rootNode, false);
- Object.keys(config.templates).forEach((key) => {
- this.templatesManager.add(key, config.templates[key]);
- });
const options: Record = {};
Object.keys(config.options).forEach((key) => {
options[key] = this.wrapOptionValue(key, config.options[key]);
});
- if (this.templatesManager.templatesCount > 0) {
- options.integrationOptions = {
- templates: this.templatesManager.templates,
- };
- }
-
return options;
}
- public update(config: IConfigNode): void {
+ public getTemplateOptions(rootNode: IConfigNode): Record {
+ const config = buildConfig(rootNode, false);
+
+ return config.templates;
+ }
+
+ public update(config: IConfigNode, dxtemplates: DXTemplateCollection): void {
const changedOptions: [string, unknown][] = [];
const optionChangedHandler = ({ value, fullName }) => {
changedOptions.push([fullName, value]);
@@ -96,14 +89,11 @@ class OptionsManager {
this.resetOption(optionName);
});
- Object.keys(changes.templates).forEach((key) => {
- this.templatesManager.add(key, changes.templates[key]);
- });
- if (this.templatesManager.templatesCount > 0) {
+ if (Object.keys(dxtemplates).length > 0) {
this.setValue(
'integrationOptions',
{
- templates: this.templatesManager.templates,
+ templates: dxtemplates,
},
);
}
diff --git a/packages/devextreme-react/src/core/template-manager.tsx b/packages/devextreme-react/src/core/template-manager.tsx
new file mode 100644
index 000000000000..c3b83178bc13
--- /dev/null
+++ b/packages/devextreme-react/src/core/template-manager.tsx
@@ -0,0 +1,180 @@
+import * as React from 'react';
+import * as events from 'devextreme/events';
+
+import {
+ useState,
+ useMemo,
+ useCallback,
+ useRef,
+ FC,
+} from 'react';
+
+import {
+ TemplateManagerProps,
+ GetRenderFuncFn,
+ DXTemplateCollection,
+ TemplateFunc,
+} from './types';
+
+import { TemplateWrapper } from './template-wrapper';
+import { TemplateInstantiationModels, generateID } from './helpers';
+import { DX_REMOVE_EVENT } from './component-base';
+import { ITemplateArgs } from './template';
+import { getOption as getConfigOption } from './config';
+import { ITemplate } from './configuration/config-node';
+
+function normalizeProps(props: ITemplateArgs): ITemplateArgs | ITemplateArgs['data'] {
+ if (getConfigOption('useLegacyTemplateEngine')) {
+ const model = props.data;
+ if (model && Object.prototype.hasOwnProperty.call(model, 'key')) {
+ model.dxkey = model.key;
+ }
+ return model;
+ }
+ return props;
+}
+
+export const TemplateManager: FC = ({ init }) => {
+ const [instantiationModels, setInstantiationModels] = useState(new TemplateInstantiationModels());
+ const [templateFactories, setTemplateFactories] = useState>({});
+ const widgetId = useRef('');
+
+ const subscribeOnRemoval = useCallback((container: HTMLElement, onRemoved: () => void) => {
+ if (container.nodeType === Node.ELEMENT_NODE) {
+ events.on(container, DX_REMOVE_EVENT, onRemoved);
+ }
+ }, []);
+
+ const unsubscribeOnRemoval = useCallback((container: HTMLElement, onRemoved: () => void) => {
+ if (container.nodeType === Node.ELEMENT_NODE) {
+ events.off(container, DX_REMOVE_EVENT, onRemoved);
+ }
+ }, []);
+
+ const createMapKey = useCallback((key1: any, key2: HTMLElement) => ({ key1, key2 }), []);
+
+ const getRandomId = useCallback(() => `${generateID()}${generateID()}${generateID()}`, []);
+
+ const getRenderFunc: GetRenderFuncFn = useCallback((templateKey) => ({
+ model: data,
+ index,
+ container,
+ onRendered,
+ }) => {
+ const key = createMapKey(data, container);
+
+ const onRemoved = (): void => {
+ setInstantiationModels((currentInstantiationModels) => {
+ const template = currentInstantiationModels.get(key);
+
+ if (template) {
+ currentInstantiationModels.delete(key);
+
+ return currentInstantiationModels.shallowCopy();
+ }
+
+ return currentInstantiationModels;
+ });
+ };
+
+ const hostWidgetId = widgetId.current;
+
+ setInstantiationModels((currentInstantiationModels) => {
+ currentInstantiationModels.set(key, {
+ templateKey,
+ index,
+ componentKey: getRandomId(),
+ onRendered: () => {
+ unsubscribeOnRemoval(container, onRemoved);
+
+ if (hostWidgetId === widgetId.current) {
+ onRendered?.();
+ }
+ },
+ onRemoved,
+ });
+
+ return currentInstantiationModels.shallowCopy();
+ });
+
+ return container;
+ }, [unsubscribeOnRemoval, createMapKey]);
+
+ useMemo(() => {
+ function getTemplateFunction(template: ITemplate): TemplateFunc {
+ switch (template.type) {
+ case 'children': return () => template.content as JSX.Element;
+
+ case 'render': return (props) => {
+ normalizeProps(props);
+ return template.content(props.data, props.index) as JSX.Element;
+ };
+
+ case 'component': return (props) => {
+ props = normalizeProps(props);
+ return React.createElement.bind(null, template.content)(props) as JSX.Element;
+ };
+
+ default: return () => React.createElement(React.Fragment);
+ }
+ }
+
+ function getDXTemplates(templateOptions: Record): DXTemplateCollection {
+ const factories = Object.entries(templateOptions)
+ .reduce((res, [key, template]) => (
+ {
+ ...res,
+ [key]: getTemplateFunction(template),
+ }
+ ), {});
+
+ setTemplateFactories(factories);
+
+ const dxTemplates = Object.keys(factories)
+ .reduce((templates, templateKey) => {
+ templates[templateKey] = { render: getRenderFunc(templateKey) };
+
+ return templates;
+ }, {});
+
+ return dxTemplates;
+ }
+
+ function clearInstantiationModels(): void {
+ widgetId.current = getRandomId();
+ setInstantiationModels(new TemplateInstantiationModels());
+ }
+
+ init(getDXTemplates, clearInstantiationModels);
+ }, [init, getRenderFunc]);
+
+ if (instantiationModels.empty) {
+ return null;
+ }
+
+ return (
+ <>
+ {
+ Array.from(instantiationModels).map(([{ key1: data, key2: container }, {
+ index,
+ templateKey,
+ componentKey,
+ onRendered,
+ onRemoved,
+ }]) => {
+ subscribeOnRemoval(container, onRemoved);
+
+ return ;
+ })
+ }
+ >
+ );
+};
diff --git a/packages/devextreme-react/src/core/template-wrapper.ts b/packages/devextreme-react/src/core/template-wrapper.ts
deleted file mode 100644
index 8434574fc03d..000000000000
--- a/packages/devextreme-react/src/core/template-wrapper.ts
+++ /dev/null
@@ -1,137 +0,0 @@
-import * as events from 'devextreme/events';
-import * as React from 'react';
-import * as ReactDOM from 'react-dom';
-
-import { DX_REMOVE_EVENT } from './component-base';
-
-interface ITemplateWrapperProps {
- content: any;
- container: Element;
- onRemoved: () => void;
- onDidMount?: () => void;
- key: string;
-}
-
-interface ITemplateWrapperState {
- removalListenerRequired: boolean;
-}
-
-type TemplateWrapperRenderer = () => TemplateWrapper;
-
-const removalListenerStyle = { display: 'none' };
-
-enum TableNodeNames {
- TABLE = 'tbody',
- TBODY = 'tr',
-}
-
-class TemplateWrapper extends React.PureComponent {
- private readonly _removalListenerRef = React.createRef();
-
- private element: HTMLElement | undefined | null;
-
- private hiddenElement: HTMLElement | undefined | null;
-
- constructor(props: ITemplateWrapperProps) {
- super(props);
-
- this.state = { removalListenerRequired: false };
-
- this._onDxRemove = this._onDxRemove.bind(this);
- this.getPreviousSiblingNode = this.getPreviousSiblingNode.bind(this);
- }
-
- public componentDidMount(): void {
- this._subscribeOnRemove();
- this.props.onDidMount?.();
- }
-
- public componentDidUpdate(): void {
- this._subscribeOnRemove();
- }
-
- public componentWillUnmount(): void {
- // Let React remove it itself
- const node = this.element;
- const hiddenNode = this.hiddenElement;
- const { container } = this.props;
-
- if (node) {
- container.appendChild(node);
- }
- if (hiddenNode) {
- container.appendChild(hiddenNode);
- }
- if (this._listenerElement) {
- container.appendChild(this._listenerElement);
- }
- }
-
- private get _listenerElement(): HTMLElement {
- return this._removalListenerRef.current as HTMLElement;
- }
-
- private getPreviousSiblingNode(node: HTMLDivElement | null) {
- this.hiddenElement = node;
- this.element = node?.previousSibling as HTMLElement;
- }
-
- private _subscribeOnRemove() {
- const node = this.element;
- const { removalListenerRequired } = this.state;
-
- if (node && node.nodeType === Node.ELEMENT_NODE) {
- this._subscribeOnElementRemoval(node as Element);
- return;
- }
-
- if (!removalListenerRequired) {
- this.setState({ removalListenerRequired: true });
- return;
- }
-
- if (this._listenerElement) {
- this._subscribeOnElementRemoval(this._listenerElement);
- }
- }
-
- private _subscribeOnElementRemoval(element: Element): void {
- events.off(element, DX_REMOVE_EVENT, this._onDxRemove);
- events.one(element, DX_REMOVE_EVENT, this._onDxRemove);
- }
-
- private _onDxRemove(): void {
- const { onRemoved } = this.props;
- onRemoved();
- }
-
- public render(): React.ReactNode {
- const { removalListenerRequired } = this.state;
- const { content, container } = this.props;
- const removalListener = removalListenerRequired
- ? React.createElement('span', { style: removalListenerStyle, ref: this._removalListenerRef })
- : undefined;
- const nodeName = TableNodeNames[container.nodeName] || 'div';
-
- // @ts-expect-error TS2322
- return ReactDOM.createPortal(
- React.createElement(
- React.Fragment,
- null,
- content,
- content && React.createElement(
- nodeName,
- { style: { display: 'none' }, ref: this.getPreviousSiblingNode },
- ),
- removalListener,
- ),
- container,
- );
- }
-}
-
-export {
- ITemplateWrapperProps,
- TemplateWrapper,
- TemplateWrapperRenderer,
-};
diff --git a/packages/devextreme-react/src/core/template-wrapper.tsx b/packages/devextreme-react/src/core/template-wrapper.tsx
new file mode 100644
index 000000000000..8d2aa611b956
--- /dev/null
+++ b/packages/devextreme-react/src/core/template-wrapper.tsx
@@ -0,0 +1,130 @@
+import * as React from 'react';
+import * as events from 'devextreme/events';
+
+import {
+ useCallback,
+ useLayoutEffect,
+ useEffect,
+ useState,
+ useRef,
+ useMemo,
+ memo,
+ FC,
+} from 'react';
+
+import { createPortal } from 'react-dom';
+import { DX_REMOVE_EVENT } from './component-base';
+import { DXRemoveCustomArgs, TemplateWrapperProps } from './types';
+import { RemovalLockerContext } from './helpers';
+
+const createHiddenNode = (
+ containerNodeName: string,
+ ref: React.LegacyRef,
+ defaultElement: string,
+) => {
+ const style = { display: 'none' };
+ switch (containerNodeName) {
+ case 'TABLE':
+ return ;
+ case 'TBODY':
+ return
;
+ default:
+ return React.createElement(defaultElement, { style, ref });
+ }
+};
+
+const TemplateWrapperComponent: FC = ({
+ templateFactory,
+ data,
+ index,
+ container,
+ onRemoved,
+ onRendered,
+}) => {
+ const [removalListenerRequired, setRemovalListenerRequired] = useState(false);
+ const isRemovalLocked = useRef(false);
+ const removalLocker = useMemo(() => ({
+ lock(): void { isRemovalLocked.current = true; },
+ unlock(): void { isRemovalLocked.current = false; },
+ }), []);
+
+ const element = useRef();
+ const hiddenNodeElement = useRef();
+ const removalListenerElement = useRef();
+
+ const onTemplateRemoved = useCallback((_, args: DXRemoveCustomArgs | undefined) => {
+ if (args?.isUnmounting || isRemovalLocked) {
+ return;
+ }
+
+ if (element.current) {
+ events.off(element.current, DX_REMOVE_EVENT, onTemplateRemoved);
+ }
+
+ if (removalListenerElement.current) {
+ events.off(removalListenerElement.current, DX_REMOVE_EVENT, onTemplateRemoved);
+ }
+
+ onRemoved();
+ }, [onRemoved]);
+
+ useLayoutEffect(() => {
+ const el = element.current;
+
+ if (el) {
+ events.off(el, DX_REMOVE_EVENT, onTemplateRemoved);
+ events.on(el, DX_REMOVE_EVENT, onTemplateRemoved);
+ }
+
+ if (!removalListenerRequired) {
+ setRemovalListenerRequired(true);
+ } else if (removalListenerElement.current) {
+ events.off(removalListenerElement.current, DX_REMOVE_EVENT, onTemplateRemoved);
+ events.on(removalListenerElement.current, DX_REMOVE_EVENT, onTemplateRemoved);
+ }
+
+ return () => {
+ if (element.current) {
+ container.appendChild(element.current);
+ }
+
+ if (hiddenNodeElement.current) {
+ container.appendChild(hiddenNodeElement.current);
+ }
+
+ if (removalListenerElement.current) {
+ container.appendChild(removalListenerElement.current);
+ }
+
+ if (el) {
+ events.off(el, DX_REMOVE_EVENT, onTemplateRemoved);
+ }
+ };
+ }, [onTemplateRemoved, removalListenerRequired, container]);
+
+ useEffect(() => {
+ onRendered();
+ }, [onRendered]);
+
+ const hiddenNode = createHiddenNode(container?.nodeName, (node: HTMLElement) => {
+ hiddenNodeElement.current = node;
+ element.current = node?.previousSibling as HTMLElement;
+ }, 'div');
+
+ const removalListener = removalListenerRequired
+ ? createHiddenNode(container?.nodeName, (node: HTMLElement) => { removalListenerElement.current = node; }, 'span')
+ : undefined;
+
+ return createPortal(
+ <>
+
+ { templateFactory({ data, index, onRendered }) }
+ { hiddenNode }
+ { removalListener }
+
+ >,
+ container,
+ );
+};
+
+export const TemplateWrapper = memo(TemplateWrapperComponent);
diff --git a/packages/devextreme-react/src/core/templates-manager.ts b/packages/devextreme-react/src/core/templates-manager.ts
deleted file mode 100644
index bd55e87684ff..000000000000
--- a/packages/devextreme-react/src/core/templates-manager.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import * as React from 'react';
-
-import { getOption as getConfigOption } from './config';
-import { ITemplate } from './configuration/config-node';
-import { createDxTemplate } from './dx-template';
-import { ITemplateArgs } from './template';
-import { TemplatesStore } from './templates-store';
-
-function normalizeProps(props: ITemplateArgs): ITemplateArgs | ITemplateArgs['data'] {
- if (getConfigOption('useLegacyTemplateEngine')) {
- const model = props.data;
- if (model && Object.prototype.hasOwnProperty.call(model, 'key')) {
- model.dxkey = model.key;
- }
- return model;
- }
- return props;
-}
-
-type ContentGetter = () => any;
-
-const contentCreators = {
- component: (contentGetter: ContentGetter) => (props: ITemplateArgs) => {
- props = normalizeProps(props);
- return React.createElement.bind(null, contentGetter())(props);
- },
- render: (contentGetter: ContentGetter) => (props: ITemplateArgs) => {
- normalizeProps(props);
- return contentGetter()(props.data, props.index);
- },
- children: (contentGetter: ContentGetter) => () => contentGetter(),
-};
-
-class TemplatesManager {
- private readonly _templatesStore: TemplatesStore;
-
- private _templates: Record = {};
-
- private _templatesContent: Record = {};
-
- constructor(templatesStore: TemplatesStore) {
- this._templatesStore = templatesStore;
- }
-
- public add(name: string, template: ITemplate): void {
- this._templatesContent[name] = template.content;
- const contentCreator = contentCreators[template.type]
- .bind(this, () => this._templatesContent[name]);
- this._templates[name] = createDxTemplate(
- contentCreator,
- this._templatesStore,
- template.keyFn,
- );
- }
-
- public get templatesCount(): number {
- return Object.keys(this._templates).length;
- }
-
- public get templates(): Record {
- return this._templates;
- }
-}
-
-export default TemplatesManager;
diff --git a/packages/devextreme-react/src/core/templates-renderer.tsx b/packages/devextreme-react/src/core/templates-renderer.tsx
deleted file mode 100644
index 28010d1b778d..000000000000
--- a/packages/devextreme-react/src/core/templates-renderer.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { requestAnimationFrame } from 'devextreme/animation/frame';
-import { deferUpdate } from 'devextreme/core/utils/common';
-import * as React from 'react';
-
-import { TemplatesStore } from './templates-store';
-
-class TemplatesRenderer extends React.PureComponent<{
- templatesStore: TemplatesStore;
-}> {
- private updateScheduled = false;
-
- private mounted = false;
-
- private shouldRepeatForceUpdate = false;
-
- private isUpdateFuncLaunched = false;
-
- componentDidMount(): void {
- this.mounted = true;
- }
-
- componentWillUnmount(): void {
- this.mounted = false;
- }
-
- public scheduleUpdate(useDeferUpdate: boolean, onRendered?: () => void): void {
- if (this.updateScheduled) {
- this.shouldRepeatForceUpdate = this.isUpdateFuncLaunched;
- return;
- }
-
- this.updateScheduled = true;
-
- const updateFunc = useDeferUpdate ? deferUpdate : requestAnimationFrame;
-
- // eslint-disable-next-line no-void
- void updateFunc(() => {
- if (this.mounted) {
- this.isUpdateFuncLaunched = true;
-
- this.forceUpdate(() => {
- this.updateScheduled = false;
- onRendered?.();
-
- if (this.shouldRepeatForceUpdate) {
- this.shouldRepeatForceUpdate = false;
- this.forceUpdate();
- }
- });
- }
-
- this.isUpdateFuncLaunched = false;
- this.updateScheduled = false;
- });
- }
-
- public render(): React.ReactNode {
- const { templatesStore } = this.props;
- return React.createElement(
- React.Fragment,
- {},
- // @ts-expect-error TS2769
- templatesStore.renderWrappers(),
- );
- }
-}
-
-export {
- TemplatesRenderer,
-};
diff --git a/packages/devextreme-react/src/core/templates-store.ts b/packages/devextreme-react/src/core/templates-store.ts
deleted file mode 100644
index e78c4b04c1af..000000000000
--- a/packages/devextreme-react/src/core/templates-store.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { TemplateWrapper, TemplateWrapperRenderer } from './template-wrapper';
-
-interface TemplateInfo {
- template: TemplateWrapperRenderer;
- isDeferredRemove: boolean;
-}
-class TemplatesStore {
- private readonly _templates: Record = {};
-
- private readonly _onTemplateAdded: () => void;
-
- constructor(onTemplateAdded: () => void) {
- this._onTemplateAdded = onTemplateAdded;
- }
-
- public add(templateId: string, templateFunc: TemplateWrapperRenderer): void {
- this._templates[templateId] = { template: templateFunc, isDeferredRemove: false };
- this._onTemplateAdded();
- }
-
- public setDeferredRemove(templateId: string, isDeferredRemove: boolean): void {
- if (this._templates[templateId]) {
- this._templates[templateId].isDeferredRemove = isDeferredRemove;
- }
- }
-
- private removeDefferedTemplate(): void {
- Object.entries(this._templates)
- .filter(([, templateInfo]) => templateInfo.isDeferredRemove)
- .forEach(([templateId]) => {
- delete this._templates[templateId];
- });
- }
-
- public renderWrappers(): TemplateWrapper[] {
- this.removeDefferedTemplate();
- return Object.getOwnPropertyNames(this._templates).map(
- (templateId) => this._templates[templateId].template(),
- );
- }
-}
-
-export {
- TemplatesStore,
-};
diff --git a/packages/devextreme-react/src/core/types.ts b/packages/devextreme-react/src/core/types.ts
new file mode 100644
index 000000000000..52cff754f4fc
--- /dev/null
+++ b/packages/devextreme-react/src/core/types.ts
@@ -0,0 +1,58 @@
+import { ITemplate } from './configuration/config-node';
+
+interface DXTemplate {
+ render: RenderFunc;
+}
+
+type RenderFunc = (arg: RenderArgs) => HTMLElement;
+
+interface TemplateArgs {
+ data: any;
+ index?: number;
+ onRendered: () => void;
+}
+
+export interface RenderArgs {
+ model?: any;
+ container: any;
+ index?: any;
+ onRendered?: () => void;
+}
+
+export interface UpdateLocker {
+ lock: () => void;
+ unlock: () => void;
+}
+
+export type DXTemplateCollection = Record;
+
+export interface TemplateWrapperProps {
+ templateFactory: TemplateFunc;
+ data: any;
+ index: number;
+ container: HTMLElement;
+ onRendered: () => void;
+ onRemoved: () => void;
+}
+
+export type TemplateFunc = (arg: TemplateArgs) => JSX.Element;
+
+export type DXTemplateCreator = (templateOptions: Record) => DXTemplateCollection;
+
+export interface TemplateManagerProps {
+ init: (getDXTemplates: DXTemplateCreator, clearInstantiationModels: () => void) => void;
+}
+
+export interface TemplateInstantiationModel {
+ templateKey: string;
+ componentKey: string;
+ index: any;
+ onRendered: () => void;
+ onRemoved: () => void;
+}
+
+export type GetRenderFuncFn = (templateKey: string) => RenderFunc;
+
+export interface DXRemoveCustomArgs {
+ isUnmounting: boolean;
+}
diff --git a/packages/devextreme-react/tsconfig.json b/packages/devextreme-react/tsconfig.json
index 07d8e9d737fd..35920bbf74c7 100644
--- a/packages/devextreme-react/tsconfig.json
+++ b/packages/devextreme-react/tsconfig.json
@@ -3,7 +3,8 @@
"compilerOptions": {
"baseUrl": "./",
- "skipLibCheck": true
+ "skipLibCheck": true,
+ "downlevelIteration": true
},
"include": [