Repository: DevExpress/devextreme-react
Pull request #72 (2018-04-21 12:56:35)

Add Template component

lukyanovas: These changes fix to update the state inside the templates, so I merge this PR immediately.

Patch:
diff --git a/example/form-example.tsx b/example/form-example.tsx
index b3b9c42..cb8b00b 100644
--- a/example/form-example.tsx
+++ b/example/form-example.tsx
@@ -2,6 +2,7 @@ import * as React from "react";
 import * as ReactDOM from "react-dom";
 import Example from "./example-block";
 
+import { Template } from "../src/core/template";
 import { Form } from "../src/ui/form";
 import { TextArea } from "../src/ui/text-area";
 
@@ -68,9 +69,7 @@ const items: any = ["ID", {
 }, {
     colSpan: 2,
     dataField: "Notes",
-    template: (data: any, container: any) => {
-        ReactDOM.render(<TextArea/>, container);
-    }
+    template: "area",
 }, "Address", {
     dataField: "Phone",
     editorOptions: {
@@ -88,7 +87,9 @@ export default class extends React.Component<any, {}> {
                     colCount={2}
                     formData={employee}
                     items={items}
-                />
+                >
+                <Template name="area" component={TextArea}/>
+                </Form>
             </Example>
         );
     }
diff --git a/src/core/component.ts b/src/core/component.ts
index 4a0e19c..0b4b591 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -2,6 +2,7 @@ import * as React from "react";
 
 import * as events from "devextreme/events";
 
+import { Template } from "./template";
 import { prepareTemplate } from "./template-provider";
 
 const DX_REMOVE_EVENT = "dxremove";
@@ -37,7 +38,7 @@ export default class Component<P> extends React.PureComponent<P, any> {
     const splitProps = this._splitComponentProps(nextProps);
     const options: Record<string, any> = {
       ...splitProps.options,
-      ...this._getIntegrationOptions(splitProps.templates)
+      ...this._getIntegrationOptions(splitProps.templates, splitProps.nestedTemplates)
     };
 
     this._processChangedValues(options, this.props);
@@ -66,7 +67,7 @@ export default class Component<P> extends React.PureComponent<P, any> {
       templatesRenderAsynchronously: true,
       ...splitProps.defaults,
       ...splitProps.options,
-      ...this._getIntegrationOptions(splitProps.templates)
+      ...this._getIntegrationOptions(splitProps.templates, splitProps.nestedTemplates)
     };
 
     this._instance = new this._WidgetClass(this._element, options);
@@ -106,18 +107,16 @@ export default class Component<P> extends React.PureComponent<P, any> {
   private _splitComponentProps(props: Record<string, any>): {
     defaults: Record<string, any>,
     options: Record<string, any>,
-    templates: Record<string, any>
+    templates: Record<string, any>,
+    nestedTemplates: Record<string, any>
   } {
     const defaults: Record<string, any> = {};
     const options: Record<string, any> = {};
     const templates: Record<string, any> = {};
+    const nestedTemplates: Record<string, any> = {};
 
     const knownTemplates: Record<string, any> = {};
 
-    const reactProps: Record<string, any> = {
-      children: true
-    };
-
     this._templateProps.forEach((value) => {
       knownTemplates[value.component] = true;
       knownTemplates[value.render] = true;
@@ -130,12 +129,21 @@ export default class Component<P> extends React.PureComponent<P, any> {
         defaults[defaultOptionName] = props[key];
       } else if (knownTemplates[key]) {
         templates[key] = props[key];
-      } else if (!reactProps[key]) {
+      } else if (key === "children") {
+        React.Children.forEach(props[key], (child) => {
+            if (child.type === Template) {
+                nestedTemplates[child.props.name] = {
+                    render: child.props.render,
+                    component: child.props.component
+                };
+            }
+        });
+      } else {
         options[key] = this._wrapEventHandler(props, key);
       }
     });
 
-    return { defaults, options, templates };
+    return { defaults, options, templates, nestedTemplates };
   }
 
   private _wrapEventHandler(options: Record<string, any>, key: string): any {
@@ -174,7 +182,7 @@ export default class Component<P> extends React.PureComponent<P, any> {
     }
   }
 
-  private _getIntegrationOptions(options: Record<string, any>): any {
+  private _getIntegrationOptions(options: Record<string, any>, nestedOptions: Record<string, any>): any {
     const templates: Record<string, any> = {};
     const result: Record<string, any> = {
       integrationOptions: {
@@ -182,17 +190,25 @@ export default class Component<P> extends React.PureComponent<P, any> {
       }
     };
 
+    const getTemplate = (component: any, render: any) => {
+        const templateProp = !!component ?
+          React.createElement.bind(this, component) :
+          render.bind(this);
+
+        return prepareTemplate(templateProp, this);
+    };
+
     this._templateProps.forEach((m) => {
       if (options[m.component] || options[m.render]) {
         result[m.tmplOption] = m.tmplOption;
-        const templateProp = !!options[m.component] ?
-          React.createElement.bind(this, options[m.component]) :
-          options[m.render].bind(this);
-
-        templates[m.tmplOption] = prepareTemplate(templateProp, this);
+        templates[m.tmplOption] = getTemplate(options[m.component], options[m.render]);
       }
     });
 
+    Object.keys(nestedOptions).forEach((name) => {
+        templates[name] = getTemplate(nestedOptions[name].component, nestedOptions[name].render);
+    });
+
     if (Object.keys(templates).length > 0) {
       return result;
     }
diff --git a/src/core/template-provider.ts b/src/core/template-provider.ts
index bc0149f..bef8b95 100644
--- a/src/core/template-provider.ts
+++ b/src/core/template-provider.ts
@@ -28,8 +28,7 @@ export function prepareTemplate(tmplFn: any, component: React.Component): { rend
 
       const templateWrapper: any = () =>
         React.createElement(TemplateWrapper, {
-          render: tmplFn,
-          data: data.model,
+          content: tmplFn(data.model),
           container: unwrapElement(data.container),
           onRemoved: removedHandler,
           key: templateId
diff --git a/src/core/template-wrapper.ts b/src/core/template-wrapper.ts
index f883621..bf14334 100644
--- a/src/core/template-wrapper.ts
+++ b/src/core/template-wrapper.ts
@@ -6,13 +6,12 @@ import * as events from "devextreme/events";
 const DX_REMOVE_EVENT = "dxremove";
 
 export default class TemplateWrapper extends React.PureComponent<{
-    render: any
-    data: any,
+    content: any
     container: Element,
     onRemoved: () => void
 }, any> {
     public render() {
-        return ReactDOM.createPortal(this.props.render(this.props.data), this.props.container);
+        return ReactDOM.createPortal(this.props.content, this.props.container);
     }
 
     public componentDidMount() {
diff --git a/src/core/template.ts b/src/core/template.ts
new file mode 100644
index 0000000..2f3a6d9
--- /dev/null
+++ b/src/core/template.ts
@@ -0,0 +1,18 @@
+import { PropTypes } from "prop-types";
+import * as React from "react";
+
+export class Template extends React.PureComponent<{
+    name: string;
+    component?: any;
+    render?: any;
+}, any> {
+    public render() {
+        return null;
+    }
+}
+
+(Template as any).propTypes = {
+    name: PropTypes.string.isRequired,
+    component: PropTypes.func,
+    render: PropTypes.func
+};


Test patch:
diff --git a/src/core/component.test.tsx b/src/core/component.test.tsx
index d7068e3..9e0038a 100644
--- a/src/core/component.test.tsx
+++ b/src/core/component.test.tsx
@@ -3,6 +3,7 @@ import { configure as configureEnzyme, mount, shallow } from "enzyme";
 import * as Adapter from "enzyme-adapter-react-16";
 import * as React from "react";
 import Component from "../core/component";
+import { Template } from "../core/template";
 
 const eventHandlers: { [index: string]: (e?: any) => void}  = {};
 const Widget = {
@@ -207,6 +208,33 @@ describe("templates", () => {
         });
     });
 
+    describe("nested template", () => {
+        it("pass integrationOptions to widget", () => {
+            const ItemTemplate = () => <div>Template</div>;
+            mount(
+                <ComponentWithTemplates>
+                    <Template name={"item1"} render={ItemTemplate}/>
+                    <Template name={"item2"} component={ItemTemplate}/>
+                </ComponentWithTemplates >
+            );
+
+            const options = WidgetClass.mock.calls[0][1];
+
+            expect(options.item).toBeUndefined();
+
+            const integrationOptions = options.integrationOptions;
+
+            expect(integrationOptions).toBeDefined();
+            expect(integrationOptions.templates).toBeDefined();
+
+            expect(integrationOptions.templates.item1).toBeDefined();
+            expect(typeof integrationOptions.templates.item1.render).toBe("function");
+
+            expect(integrationOptions.templates.item2).toBeDefined();
+            expect(typeof integrationOptions.templates.item2.render).toBe("function");
+        });
+    });
+
     it("pass component option changes to widget", () => {
         const ItemTemplate = () => <div>First Template</div>;
         const component = mount(