Repository: DevExpress/devextreme-react
Pull request #315 (2019-02-22 17:09:12)

Refactor  templates 
Relates to #313 

IlyaKhD: @nightskylark, this commit should go next (in a separate PR): https://github.com/IlyaKhD/devextreme-react/commit/388fcf6849ae6a4a595edc9b748d76790eb3c79d

Patch:
diff --git a/src/core/component-base.ts b/src/core/component-base.ts
index 8e2b269..9a6e62a 100644
--- a/src/core/component-base.ts
+++ b/src/core/component-base.ts
@@ -1,14 +1,13 @@
 import * as events from "devextreme/events";
 
-import * as commonUtils from "devextreme/core/utils/common";
-
 import * as React from "react";
 
 import OptionsManager, { INestedOption } from "./options-manager";
 import { findProps as findNestedTemplateProps, ITemplateMeta } from "./template";
-import { elementPropNames, getClassName, separateProps } from "./widget-config";
-
 import TemplateHost from "./template-host";
+import { TemplateUpdater } from "./template-updater";
+import { TemplateWrapperRenderer } from "./template-wrapper";
+import { elementPropNames, getClassName, separateProps } from "./widget-config";
 
 const DX_REMOVE_EVENT = "dxremove";
 
@@ -18,7 +17,7 @@ interface IWidgetConfig {
 }
 
 interface IState {
-  templates: Record<string, () => void>;
+  templates: Record<string, TemplateWrapperRenderer>;
 }
 
 interface IHtmlOptions {
@@ -36,22 +35,23 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
   protected readonly _templateProps: ITemplateMeta[] = [];
   protected readonly _expectedChildren: Record<string, INestedOption>;
 
-  private _templateCallbacks: any[] = [];
-  private _isTemplatePostponed: boolean = false;
-
   private readonly _templateHost: TemplateHost;
   private readonly _optionsManager: OptionsManager;
+  private readonly _templateUpdater: TemplateUpdater;
 
   constructor(props: P) {
     super(props);
     this._prepareProps = this._prepareProps.bind(this);
-    this._updateTemplatesState = this._updateTemplatesState.bind(this);
 
     this.state = {
       templates: {}
     };
 
-    this._templateHost = new TemplateHost(this._updateTemplatesState);
+    this._templateUpdater = new TemplateUpdater((templates) => {
+      this.setState({ templates: {...templates} });
+    });
+
+    this._templateHost = new TemplateHost(this._templateUpdater);
     this._optionsManager = new OptionsManager((name) => this.props[name], this._templateHost);
   }
 
@@ -76,7 +76,6 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
   }
 
   public componentWillUnmount() {
-    this._clearTemplates();
     if (this._instance) {
       events.triggerHandler(this._element, DX_REMOVE_EVENT);
       this._instance.dispose();
@@ -132,29 +131,6 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
     this._instance.on("optionChanged", this._optionsManager.handleOptionChange);
   }
 
-  private _updateTemplatesState(callback: any) {
-    this._templateCallbacks.push(callback);
-
-    if (this._isTemplatePostponed) { return; }
-
-    this._isTemplatePostponed = true;
-    commonUtils.deferUpdate(() => {
-      this.setState((state: IState) => {
-        const templates = { ...state.templates };
-        for (const cb of this._templateCallbacks) {
-          cb(templates);
-        }
-        this._clearTemplates();
-        return { templates };
-      });
-    });
-  }
-
-  private _clearTemplates() {
-    this._templateCallbacks.length = 0;
-    this._isTemplatePostponed = false;
-  }
-
   private _getElementProps(): Record<string, any> {
     const elementProps: Record<string, any> = {
       ref: (element: HTMLDivElement) => this._element = element
diff --git a/src/core/dx-template.ts b/src/core/dx-template.ts
new file mode 100644
index 0000000..320912e
--- /dev/null
+++ b/src/core/dx-template.ts
@@ -0,0 +1,65 @@
+import * as React from "react";
+
+import { generateID } from "./helpers";
+import { ITemplateUpdater } from "./template-updater";
+import { ITemplateWrapperProps, TemplateWrapper } from "./template-wrapper";
+
+interface IDxTemplate {
+    render: (data: IDxTemplateData) => any;
+}
+
+interface IDxTemplateData {
+    container: any;
+    model?: any;
+    index?: any;
+    onRendered?: () => void;
+}
+
+function createDxTemplate(
+    createContentProvider: () => (model: any) => any,
+    templateUpdater: ITemplateUpdater,
+    keyFn?: (data: any) => string
+): IDxTemplate {
+
+    const renderedContainers: HTMLElement[] = [];
+    return {
+        render: (data: IDxTemplateData) => {
+            const templateId = keyFn ? keyFn(data.model) : "__template_" + generateID();
+            const container = unwrapElement(data.container);
+
+            if (renderedContainers.indexOf(container) > -1) {
+                return container;
+            }
+            renderedContainers.push(container);
+
+            templateUpdater.setTemplate(templateId, () => {
+                const model = data.model;
+                if (model && model.hasOwnProperty("key")) {
+                    model.dxkey = model.key;
+                }
+                const contentProvider = createContentProvider();
+                return React.createElement<ITemplateWrapperProps>(
+                    TemplateWrapper,
+                    {
+                        content: contentProvider(model),
+                        container,
+                        onRemoved: () => templateUpdater.removeTemplate(templateId),
+                        onRendered: data.onRendered,
+                        key: templateId
+                    }
+                ) as any as TemplateWrapper;
+            });
+
+            return container;
+        }
+    };
+}
+
+function unwrapElement(element: any): HTMLElement {
+    return element.get ? element.get(0) : element;
+}
+
+export {
+    IDxTemplate,
+    createDxTemplate
+};
diff --git a/src/core/template-host.ts b/src/core/template-host.ts
index aa9d8ec..a32f011 100644
--- a/src/core/template-host.ts
+++ b/src/core/template-host.ts
@@ -1,25 +1,11 @@
-import { ITemplateMeta, ITemplateProps } from "./template";
-
 import * as React from "react";
 
-import { generateID } from "./helpers";
-import { ITemplateWrapperProps, TemplateWrapper } from "./template-wrapper";
+import { createDxTemplate, IDxTemplate } from "./dx-template";
+import { ITemplateMeta, ITemplateProps } from "./template";
+import { ITemplateUpdater } from "./template-updater";
 
-type TemplateGetter = (nestedTemplates: Record<string, any>) => React.ReactElement<ITemplateWrapperProps>;
-type StateUpdater = (callback: (templates: TemplateGetter) => void) => void;
 type PropsGetter = (propName: string) => any;
 
-interface IDxTemplateData {
-    container: any;
-    model?: any;
-    index?: any;
-    onRendered?: () => void;
-}
-
-interface IDxTemplate {
-    render: (data: IDxTemplateData) => any;
-}
-
 interface IIntegrationDescr {
     props: Record<string, any>;
     templateProps: ITemplateMeta[];
@@ -35,7 +21,7 @@ const contentCreators = {
 };
 
 class TemplateHost {
-    private readonly _stateUpdater: StateUpdater;
+    private readonly _templateUpdater: ITemplateUpdater;
 
     private _templates: Record<string, any> = {};
     private _stubs: Record<string, any> = {};
@@ -45,8 +31,8 @@ class TemplateHost {
         children: any;
     }> = {};
 
-    constructor(stateUpdater: StateUpdater) {
-        this._stateUpdater = stateUpdater;
+    constructor(templateUpdater: ITemplateUpdater) {
+        this._templateUpdater = templateUpdater;
     }
 
     public add(meta: IIntegrationDescr) {
@@ -83,7 +69,7 @@ class TemplateHost {
 
             const name = ownerName ? `${ownerName}.${tmpl.tmplOption}` : tmpl.tmplOption;
             stubs[name] = name;
-            templates[name] = wrapTemplate(contentCreator, this._stateUpdater, meta.propsGetter(tmpl.keyFn));
+            templates[name] = createDxTemplate(contentCreator, this._templateUpdater, meta.propsGetter(tmpl.keyFn));
         }
 
         this._templates = {
@@ -109,7 +95,7 @@ class TemplateHost {
         const propsGetter: PropsGetter = (prop) => this._nestedTemplateProps[name][prop];
 
         const contentCreator = contentCreators[type].bind(this, type, propsGetter);
-        this._templates[name] = wrapTemplate(contentCreator, this._stateUpdater, props.keyFn);
+        this._templates[name] = createDxTemplate(contentCreator, this._templateUpdater, props.keyFn);
     }
 
     public get options(): Record<string, any> | undefined {
@@ -126,51 +112,4 @@ class TemplateHost {
     }
 }
 
-function wrapTemplate(
-    createContentProvider: () => (model: any) => any,
-    stateUpdater: StateUpdater,
-    keyFn?: (data) => string
-): IDxTemplate {
-
-    const renderedContainers: HTMLElement[] = [];
-    return {
-        render: (data: IDxTemplateData) => {
-            const templateId = keyFn ? keyFn(data.model) : "__template_" + generateID();
-            const container = unwrapElement(data.container);
-
-            if (renderedContainers.indexOf(container) > -1) {
-                return container;
-            }
-            renderedContainers.push(container);
-
-            const createWrapper = () => {
-                const model = data.model;
-                if (model && model.hasOwnProperty("key")) {
-                    model.dxkey = model.key;
-                }
-                const contentProvider = createContentProvider();
-                return React.createElement<ITemplateWrapperProps>(
-                    TemplateWrapper,
-                    {
-                        content: contentProvider(model),
-                        container,
-                        onRemoved: () => stateUpdater((t) => delete t[templateId]),
-                        onRendered: data.onRendered,
-                        key: templateId
-                    }
-                );
-            };
-            stateUpdater((t) => t[templateId] = createWrapper);
-            return container;
-        }
-    };
-}
-
-function unwrapElement(element: any) {
-    return element.get ? element.get(0) : element;
-}
-
 export default TemplateHost;
-export {
-    wrapTemplate
-};
diff --git a/src/core/template-updater.ts b/src/core/template-updater.ts
new file mode 100644
index 0000000..3542b65
--- /dev/null
+++ b/src/core/template-updater.ts
@@ -0,0 +1,51 @@
+import * as commonUtils from "devextreme/core/utils/common";
+import { TemplateWrapperRenderer } from "./template-wrapper";
+
+interface ITemplateUpdater {
+    setTemplate(templateId: string, templateFunc: TemplateWrapperRenderer): void;
+    removeTemplate(templateId: string): void;
+}
+
+class TemplateUpdater implements ITemplateUpdater {
+
+    private readonly _templates: Record<string, TemplateWrapperRenderer> = {};
+    private readonly _updateTemplatesCallback: (t: Record<string, TemplateWrapperRenderer>) => void;
+
+    private _updateIsDelayed: boolean = false;
+
+    constructor(updateTemplatesCallback: (t: Record<string, TemplateWrapperRenderer>) => void) {
+        this._updateTemplatesCallback = updateTemplatesCallback;
+
+        this._scheduleUpdate = this._scheduleUpdate.bind(this);
+        this.setTemplate = this.setTemplate.bind(this);
+        this.removeTemplate = this.removeTemplate.bind(this);
+    }
+
+    public setTemplate(templateId: string, templateFunc: TemplateWrapperRenderer): void {
+        this._templates[templateId] = templateFunc;
+        this._scheduleUpdate();
+    }
+
+    public removeTemplate(templateId: string): void {
+        delete this._templates[templateId];
+        this._scheduleUpdate();
+    }
+
+    private _scheduleUpdate() {
+        if (this._updateIsDelayed) {
+            return;
+        }
+
+        this._updateIsDelayed = true;
+
+        commonUtils.deferUpdate(() => {
+            this._updateTemplatesCallback(this._templates);
+            this._updateIsDelayed = false;
+        });
+    }
+}
+
+export {
+    ITemplateUpdater,
+    TemplateUpdater
+};
diff --git a/src/core/template-wrapper.ts b/src/core/template-wrapper.ts
index 8fc2961..7ca14c1 100644
--- a/src/core/template-wrapper.ts
+++ b/src/core/template-wrapper.ts
@@ -12,6 +12,8 @@ interface ITemplateWrapperProps {
     key: string;
 }
 
+type TemplateWrapperRenderer = () => TemplateWrapper;
+
 class TemplateWrapper extends React.PureComponent<ITemplateWrapperProps> {
     private readonly _removalListenerRef = React.createRef<HTMLElement>();
 
@@ -32,7 +34,7 @@ class TemplateWrapper extends React.PureComponent<ITemplateWrapperProps> {
                         style: { display: "none" },
                         ref: this._removalListenerRef
                     }
-                ),
+                )
             ),
             this.props.container
         );
@@ -67,5 +69,6 @@ class TemplateWrapper extends React.PureComponent<ITemplateWrapperProps> {
 
 export {
     ITemplateWrapperProps,
-    TemplateWrapper
+    TemplateWrapper,
+    TemplateWrapperRenderer
 };


Test patch:
diff --git a/src/core/__tests__/template-host.test.tsx b/src/core/__tests__/template-dx-integration.test.tsx
similarity index 57%
rename from src/core/__tests__/template-host.test.tsx
rename to src/core/__tests__/template-dx-integration.test.tsx
index d35052b..a9adca3 100644
--- a/src/core/__tests__/template-host.test.tsx
+++ b/src/core/__tests__/template-dx-integration.test.tsx
@@ -1,15 +1,18 @@
-import { wrapTemplate } from "../template-host";
+import { createDxTemplate } from "../dx-template";
 
 describe("template-host", () => {
 
     describe("its wrapped template", () => {
 
         it("doesn't render twice into the same container", () => {
-            const updateFunc = jest.fn();
+            const setTemplate = jest.fn();
             const container: any = {};
-            const template = wrapTemplate(
+            const template = createDxTemplate(
                 jest.fn(),
-                updateFunc,
+                {
+                    setTemplate,
+                    removeTemplate: jest.fn()
+                },
                 jest.fn()
             );
 
@@ -21,7 +24,7 @@ describe("template-host", () => {
                 container
             });
 
-            expect(updateFunc).toHaveBeenCalledTimes(1);
+            expect(setTemplate).toHaveBeenCalledTimes(1);
         });
     });