Repository: DevExpress/devextreme-react
Pull request #264 (2018-12-17 10:50:46)

Support static item syntax


Patch:
diff --git a/example/data-grid-example.tsx b/example/data-grid-example.tsx
index 7c481e2..70eeb8e 100644
--- a/example/data-grid-example.tsx
+++ b/example/data-grid-example.tsx
@@ -34,10 +34,6 @@ const RegionComponent = (props: any) => {
     return <b>{props.displayValue}</b>;
 };
 
-const ToolbarItemRender = () => {
-    return <b>Collapse or Expand Groups:</b>;
-};
-
 export default class extends React.Component<any, { expandAll: boolean, pageSize: number }> {
 
     constructor(props: any) {
@@ -94,7 +90,9 @@ export default class extends React.Component<any, { expandAll: boolean, pageSize
                     />
                     <MasterDetail enabled={true} component={DetailComponent} />
 
-                    <Template name={"toolbarLabel"} render={ToolbarItemRender} />
+                    <Template name={"toolbarLabel"}>
+                        {this.state.expandAll ? <b>Collapse Groups:</b> : <b>Expand Groups:</b>}
+                    </Template>
                 </DataGrid>
             </Example>
         );
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 db9183e..41cf199 100644
--- a/src/core/template-host.ts
+++ b/src/core/template-host.ts
@@ -1,4 +1,4 @@
-import { ITemplateMeta } from "./template";
+import { ITemplateMeta, ITemplateProps } from "./template";
 
 import * as React from "react";
 
@@ -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;
 
@@ -42,6 +42,7 @@ class TemplateHost {
     private _nestedTemplateProps: Record<string, {
         render: any;
         component: any;
+        children: any;
     }> = {};
 
     constructor(stateUpdater: StateUpdater) {
@@ -57,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 = {
@@ -83,26 +97,19 @@ class TemplateHost {
         };
     }
 
-    public addNested(props: {
-        name: string;
-        render: any;
-        component: any;
-    }): void {
+    public addNested(props: ITemplateProps): void {
         const name: string = props.name;
         this._nestedTemplateProps[name] = {
             component: props.component,
-            render: props.render
+            render: props.render,
+            children: props.children
         };
-        const propsGetter = (prop) => this._nestedTemplateProps[name][prop];
 
-        const templateDescr: ITemplateDescr = {
-            name,
-            propName: !!props.component ? "component" : "render",
-            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 {
@@ -119,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,
diff --git a/src/core/template.ts b/src/core/template.ts
index 18e3e64..df87dd0 100644
--- a/src/core/template.ts
+++ b/src/core/template.ts
@@ -11,6 +11,7 @@ interface ITemplateProps {
     name: string;
     component?: any;
     render?: any;
+    children?: any;
 }
 
 class Template extends React.PureComponent<ITemplateProps, any> {
@@ -20,7 +21,7 @@ class Template extends React.PureComponent<ITemplateProps, any> {
 }
 
 const requiredPropsCheck = (props: Record<string, any>) => {
-    if (!props.component && !props.render) {
+    if (!props.component && !props.render && !props.children) {
         return new Error("The Template component requires 'component' or 'render' property");
     }
     return null;
@@ -29,22 +30,25 @@ const requiredPropsCheck = (props: Record<string, any>) => {
 (Template as any).propTypes = {
     name: PropTypes.string.isRequired,
     component: requiredPropsCheck,
-    render: requiredPropsCheck
+    render: requiredPropsCheck,
+    children: requiredPropsCheck
 };
 
-function findProps(child: React.ReactElement<any>): { name: string, render: any, component: any } | undefined {
+function findProps(child: React.ReactElement<any>): ITemplateProps | undefined {
     if (child.type !== Template) {
         return;
     }
     return {
         name: child.props.name,
         render: child.props.render,
-        component: child.props.component
+        component: child.props.component,
+        children: child.props.children
     };
 }
 
 export {
     ITemplateMeta,
+    ITemplateProps,
     Template,
     findProps
 };


Test patch:
diff --git a/src/core/__tests__/template.test.tsx b/src/core/__tests__/template.test.tsx
index 1641de9..0126a52 100644
--- a/src/core/__tests__/template.test.tsx
+++ b/src/core/__tests__/template.test.tsx
@@ -244,6 +244,9 @@ describe("nested template", () => {
             <ComponentWithTemplates>
                 <Template name={"item1"} render={ItemTemplate} />
                 <Template name={"item2"} component={ItemTemplate} />
+                <Template name={"item3"}>
+                    <ItemTemplate/>
+                </Template>
             </ComponentWithTemplates >
         );
 
@@ -261,6 +264,9 @@ describe("nested template", () => {
 
         expect(integrationOptions.templates.item2).toBeDefined();
         expect(typeof integrationOptions.templates.item2.render).toBe("function");
+
+        expect(integrationOptions.templates.item3).toBeDefined();
+        expect(typeof integrationOptions.templates.item3.render).toBe("function");
     });
 
     it("renders nested templates", () => {
@@ -275,6 +281,19 @@ describe("nested template", () => {
         expect(component.find(".template").html()).toBe('<div class="template">Template</div>');
     });
 
+    it("renders children of nested template", () => {
+        const component = mount(
+            <ComponentWithTemplates>
+                <Template name={"item1"}>
+                    <div className={"template"}>Template</div>
+                </Template>
+            </ComponentWithTemplates >
+        );
+        renderTemplate("item1");
+        component.update();
+        expect(component.find(".template").html()).toBe('<div class="template">Template</div>');
+    });
+
     it("renders new templates after component change", () => {
         const FirstTemplate = () => <div className={"template"}>First Template</div>;
         const component = mount(
@@ -294,6 +313,29 @@ describe("nested template", () => {
         expect(component.find(".template").html()).toBe('<div class="template">Second Template</div>');
     });
 
+    it("renders new templates after children change", () => {
+        const component = mount(
+            <ComponentWithTemplates>
+                <Template name={"item1"}>
+                    <div className={"template"}>First Template</div>
+                </Template>
+            </ComponentWithTemplates >
+        );
+        renderTemplate("item1");
+        component.update();
+        expect(component.find(".template").html()).toBe('<div class="template">First Template</div>');
+
+        component.setProps({
+            children: (
+                <Template name={"item1"}>
+                    <div className={"template"}>Second Template</div>
+                </Template>
+            )
+        });
+        component.update();
+        expect(component.find(".template").html()).toBe('<div class="template">Second Template</div>');
+    });
+
 });
 
 describe("component/render in nested options", () => {
@@ -407,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(
@@ -425,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>');
+    });
 });