Repository: DevExpress/devextreme-react
Pull request #260 (2018-12-11 15:00:22)

Support static item syntax

IlyaKhD: From the `template-host.ts`:
```diff
-                const propsGetter = templateDescr.propsGetter;

                const contentProvider = templateDescr.isComponent
-                    ? React.createElement.bind(null, propsGetter(templateDescr.propName))
+                   ? React.createElement.bind(null, templateDescr.propsGetter(templateDescr.propName))
-                    : propsGetter(templateDescr.propName);
+                   : templateDescr.propsGetter(templateDescr.propName);
```
Removing unnecessary variables improves "find all usages\references" experience

What is more, we should definitely try to get rid of the `templateDescr.propName`

Patch:
diff --git a/example/form-example.tsx b/example/form-example.tsx
index 5c1df61..99e0ac3 100644
--- a/example/form-example.tsx
+++ b/example/form-example.tsx
@@ -1,7 +1,6 @@
 import * as React from "react";
 import Example from "./example-block";
 
-import { Template } from "../src/core/template";
 import Form, { RequiredRule, SimpleItem, } from "../src/form";
 import TextArea from "../src/text-area";
 
@@ -58,9 +57,10 @@ export default class extends React.Component<any, {}> {
                     </SimpleItem>
                     <SimpleItem
                         dataField={"Notes"}
-                        component={TextArea}
                         colSpan={2}
-                    />
+                    >
+                        <TextArea/>
+                    </SimpleItem>
                     <SimpleItem dataField={"Address"} />
                     <SimpleItem
                         dataField={"Phone"}
diff --git a/src/core/component-base.ts b/src/core/component-base.ts
index 3a852fc..6327f6d 100644
--- a/src/core/component-base.ts
+++ b/src/core/component-base.ts
@@ -172,6 +172,7 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
     const separatedProps = separateProps(rawProps, this._defaults, this._templateProps);
 
     this._templateHost.add({
+      useChildren: () => false,
       ownerName: "",
       templateProps: this._templateProps,
       props: separatedProps.templates,
diff --git a/src/core/options-manager.ts b/src/core/options-manager.ts
index ac2dd56..d098fd1 100644
--- a/src/core/options-manager.ts
+++ b/src/core/options-manager.ts
@@ -182,6 +182,7 @@ class OptionsManager {
                     configComponent.templates);
 
                 this._templateHost.add({
+                    useChildren: (optionName) => optionName === "template" && !!e.element.props.children,
                     props: props.templates,
                     templateProps: configComponent.templates,
                     ownerName: `${configComponent.optionName}${configComponent.isCollectionItem ? `[${index}]` : ""}`,
diff --git a/src/core/template-host.ts b/src/core/template-host.ts
index e2cc35b..41cf199 100644
--- a/src/core/template-host.ts
+++ b/src/core/template-host.ts
@@ -20,20 +20,20 @@ interface IDxTemplate {
     render: (data: IDxTemplateData) => any;
 }
 
-interface ITemplateDescr {
-    name: string;
-    propName: string;
-    isComponent: boolean;
-    propsGetter: PropsGetter;
-}
-
 interface IIntegrationDescr {
     props: Record<string, any>;
     templateProps: ITemplateMeta[];
     ownerName: string;
     propsGetter: PropsGetter;
+    useChildren: (name: string) => boolean;
 }
 
+const contentCreators = {
+    component: (name: string, propsGetter: PropsGetter) => React.createElement.bind(null, propsGetter(name)),
+    render: (name: string, propsGetter: PropsGetter) => propsGetter(name),
+    children: (_: string, propsGetter: PropsGetter) => () => propsGetter("children")
+};
+
 class TemplateHost {
     private readonly _stateUpdater: StateUpdater;
 
@@ -58,19 +58,32 @@ class TemplateHost {
         const ownerName = meta.ownerName;
 
         for (const tmpl of templateProps) {
-            if (!props[tmpl.component] && !props[tmpl.render]) {
+            let contentCreator;
+            let propName;
+
+            if (meta.useChildren(tmpl.tmplOption)) {
+                contentCreator = contentCreators.children;
+            }
+
+            if (props[tmpl.render]) {
+                propName = tmpl.render;
+                contentCreator = contentCreators.render;
+            }
+
+            if (props[tmpl.component]) {
+                propName = tmpl.component;
+                contentCreator = contentCreators.component;
+            }
+
+            if (!contentCreator) {
                 continue;
             }
-            const name = ownerName ? `${ownerName}.${tmpl.tmplOption}` : tmpl.tmplOption;
 
-            const templateDescr: ITemplateDescr = {
-                name,
-                propName: props[tmpl.component] ? tmpl.component : tmpl.render,
-                isComponent: !!props[tmpl.component],
-                propsGetter: meta.propsGetter
-            };
+            contentCreator = contentCreator.bind(this, propName, meta.propsGetter);
+
+            const name = ownerName ? `${ownerName}.${tmpl.tmplOption}` : tmpl.tmplOption;
             stubs[name] = name;
-            templates[name] = wrapTemplate(templateDescr, this._stateUpdater);
+            templates[name] = wrapTemplate(contentCreator, this._stateUpdater);
         }
 
         this._templates = {
@@ -89,18 +102,14 @@ class TemplateHost {
         this._nestedTemplateProps[name] = {
             component: props.component,
             render: props.render,
-            children: () => props.children
+            children: props.children
         };
-        const propsGetter = (prop) => this._nestedTemplateProps[name][prop];
 
-        const templateDescr: ITemplateDescr = {
-            name,
-            propName: !!props.component ? "component" : !!props.render ? "render" : "children",
-            isComponent: !!props.component,
-            propsGetter
-        };
+        const type = !!props.component ? "component" : !!props.render ? "render" : "children";
+        const propsGetter: PropsGetter = (prop) => this._nestedTemplateProps[name][prop];
 
-        this._templates[name] = wrapTemplate(templateDescr, this._stateUpdater);
+        const contentCreator = contentCreators[type].bind(this, type, propsGetter);
+        this._templates[name] = wrapTemplate(contentCreator, this._stateUpdater);
     }
 
     public get options(): Record<string, any> | undefined {
@@ -117,22 +126,17 @@ class TemplateHost {
     }
 }
 
-function wrapTemplate(templateDescr: ITemplateDescr, stateUpdater: StateUpdater): IDxTemplate {
+function wrapTemplate(createContentProvider: () => (model: any) => any, stateUpdater: StateUpdater): IDxTemplate {
     return {
         render: (data: IDxTemplateData) => {
             const templateId = "__template_" + generateID();
             const container = unwrapElement(data.container);
             const createWrapper = () => {
-                const propsGetter = templateDescr.propsGetter;
-
-                const contentProvider = templateDescr.isComponent
-                    ? React.createElement.bind(null, propsGetter(templateDescr.propName))
-                    : propsGetter(templateDescr.propName);
-
                 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,


Test patch:
diff --git a/src/core/__tests__/template.test.tsx b/src/core/__tests__/template.test.tsx
index 9bab76c..0126a52 100644
--- a/src/core/__tests__/template.test.tsx
+++ b/src/core/__tests__/template.test.tsx
@@ -449,6 +449,36 @@ describe("component/render in nested options", () => {
         expect(Object.keys(integrationOptions.templates)).toEqual(["collection[0].template", "collection[1].template"]);
     });
 
+    it("pass integrationOptions for collection nested component with 'template' option if a child defined", () => {
+        const UserTemplate = () => <div>Template</div>;
+        mount(
+            <TestComponent>
+                <NestedComponent>
+                    <UserTemplate/>
+                </NestedComponent>
+                <CollectionNestedComponent>
+                    <UserTemplate/>
+                </CollectionNestedComponent>
+                <CollectionNestedComponent>
+                    <UserTemplate/>
+                </CollectionNestedComponent>
+                <CollectionNestedComponent/>
+            </TestComponent>
+        );
+
+        const options = WidgetClass.mock.calls[0][1];
+
+        expect(options["collection[0].template"]).toBe("collection[0].template");
+        expect(options["collection[1].template"]).toBe("collection[1].template");
+        expect(options["collection[2].template"]).toBe(undefined);
+        expect(options["option.item"]).toBe(undefined);
+        expect(options["option.template"]).toBe(undefined);
+
+        const integrationOptions = options.integrationOptions;
+
+        expect(Object.keys(integrationOptions.templates)).toEqual(["collection[0].template", "collection[1].template"]);
+    });
+
     it("renders templates", () => {
         const FirstTemplate = () => <div className={"template"}>First Template</div>;
         const component = mount(
@@ -467,4 +497,29 @@ describe("component/render in nested options", () => {
         component.update();
         expect(component.find(".template").html()).toBe('<div class="template">Second Template</div>');
     });
+
+    it("renders static templates", () => {
+        const FirstTemplate = () => <div className={"template"}>First Template</div>;
+        const component = mount(
+            <TestComponent>
+                <CollectionNestedComponent>
+                    <FirstTemplate/>
+                </CollectionNestedComponent>
+            </TestComponent >
+        );
+        renderTemplate("collection[0].template");
+        component.update();
+        expect(component.find(".template").html()).toBe('<div class="template">First Template</div>');
+
+        const SecondTemplate = () => <div className={"template"}>Second Template</div>;
+        component.setProps({
+            children: (
+                <CollectionNestedComponent>
+                    <SecondTemplate/>
+                </CollectionNestedComponent>
+            )
+        });
+        component.update();
+        expect(component.find(".template").html()).toBe('<div class="template">Second Template</div>');
+    });
 });