Repository: DevExpress/devextreme-react
Pull request #113 (2018-05-30 19:34:18)

Refactor component rendering

IlyaKhD: This not finished yet.

@lukyanovas, could you take a look at [these lines](https://github.com/DevExpress/devextreme-react/pull/113/files#diff-41dcde6fd45bf4c6ed1a98bd664fc811R188) and share your thoughts?
I mean using templates with `strip: true` option turned on and placeholders for new lines and tabs. I think this should make templates syntax much more clear.

Patch:
diff --git a/src/core/component.ts b/src/core/component.ts
index b0d83e9..908ab1f 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -6,7 +6,7 @@ import { addPrefixToKeys } from "./helpers";
 import { createConfigurationComponent } from "./nested-option";
 import { OptionsManager } from "./options-manager";
 import { ITemplateMeta, Template } from "./template";
-import { IDxTemplate, TemplateHelper } from "./template-helper";
+import { IDxTemplate, ITemplateInfo, IWrappedTemplateInfo, TemplateHelper } from "./template-helper";
 import { separateProps } from "./widget-config";
 
 const DX_REMOVE_EVENT = "dxremove";
@@ -29,7 +29,7 @@ interface IWidgetConfig {
 }
 
 interface IState {
-  templates: {};
+  templates: Record<string, IWrappedTemplateInfo>;
 }
 
 class Component<P> extends React.PureComponent<P, IState> {
@@ -73,15 +73,28 @@ class Component<P> extends React.PureComponent<P, IState> {
         { ref: (element: any) => this._element = element }
       ];
 
+      let nestedTemplates: Record<string, any> = {};
       if (!!this.props.children) {
-          React.Children.forEach(this.props.children, (c) => {
-            args.push(this._registerNestedOption(c as React.ReactElement<any>) || c);
+          React.Children.forEach(this.props.children, (child: React.ReactElement<any>) => {
+            nestedTemplates = {
+              ...nestedTemplates,
+              ...this.findNestedTemplate(child)
+            };
+            args.push(this._registerNestedOption(child) || child);
           });
       }
 
       const templates = Object.getOwnPropertyNames(this.state.templates);
       if (templates.length) {
-        templates.forEach((t) => args.push(this.state.templates[t]()));
+        templates.forEach((t) => {
+          const templateDto = this.state.templates[t];
+          const targetProps: Record<string, any> = templateDto.isNested
+            ? nestedTemplates[templateDto.name]
+            : this.props;
+          args.push(templateDto.createWrapper(
+            this._templateHelper.getContentProvider(targetProps[templateDto.prop], templateDto.isComponent)
+          ));
+        });
       }
 
       return React.createElement.apply(this, args);
@@ -124,16 +137,12 @@ class Component<P> extends React.PureComponent<P, IState> {
         });
     }
 
-    const nestedTemplates: Record<string, any> = {};
+    let nestedTemplates: Record<string, any> = {};
     if (rawProps.children) {
         React.Children.forEach(rawProps.children, (child: React.ReactElement<any>) => {
-            if (child.type !== Template) {
-                return;
-            }
-
-            nestedTemplates[child.props.name] = {
-                render: child.props.render,
-                component: child.props.component
+            nestedTemplates = {
+              ...nestedTemplates,
+              ...this.findNestedTemplate(child)
             };
         });
     }
@@ -145,17 +154,31 @@ class Component<P> extends React.PureComponent<P, IState> {
     };
   }
 
-    private _wrapEventHandler(optionValue: any, optionName: string): any {
-        if (optionName.substr(0, 2) === "on" && typeof optionValue === "function") {
-            return (...args: any[]) => {
-                if (!this._optionsManager.updatingProps) {
-                    optionValue(...args);
-                }
-            };
-        }
-
-        return optionValue;
+  private findNestedTemplate(child: React.ReactElement<any>): Record<string, { render: any, component: any }> {
+    if (child.type !== Template) {
+        return {};
     }
+    const result: Record<string, any> = {};
+
+    result[child.props.name] = {
+        render: child.props.render,
+        component: child.props.component
+    };
+
+    return result;
+  }
+
+  private _wrapEventHandler(optionValue: any, optionName: string): any {
+      if (optionName.substr(0, 2) === "on" && typeof optionValue === "function") {
+          return (...args: any[]) => {
+              if (!this._optionsManager.updatingProps) {
+                  optionValue(...args);
+              }
+          };
+      }
+
+      return optionValue;
+  }
 
   private _getIntegrationOptions(options: Record<string, any>, nestedOptions: Record<string, any>): any {
     const templates: Record<string, IDxTemplate> = {};
@@ -167,13 +190,25 @@ class Component<P> extends React.PureComponent<P, IState> {
 
     this._templateProps.forEach((m) => {
       if (options[m.component] || options[m.render]) {
+        const templateInfo: ITemplateInfo = {
+          name: m.tmplOption,
+          prop: options[m.component] ? m.component : m.render,
+          isComponent: !!options[m.component],
+          isNested: false
+        };
         result[m.tmplOption] = m.tmplOption;
-        templates[m.tmplOption] = this._templateHelper.wrapTemplate(options[m.component], options[m.render]);
+        templates[m.tmplOption] = this._templateHelper.wrapTemplate(templateInfo);
       }
     });
 
     Object.keys(nestedOptions).forEach((name) => {
-        templates[name] = this._templateHelper.wrapTemplate(nestedOptions[name].component, nestedOptions[name].render);
+        const templateInfo: ITemplateInfo = {
+          name,
+          prop: !!nestedOptions[name].component ? "component" : "render",
+          isComponent: !!nestedOptions[name].component,
+          isNested: true
+        };
+        templates[name] = this._templateHelper.wrapTemplate(templateInfo);
     });
 
     if (Object.keys(templates).length > 0) {
diff --git a/src/core/template-helper.ts b/src/core/template-helper.ts
index 740ea4d..049be07 100644
--- a/src/core/template-helper.ts
+++ b/src/core/template-helper.ts
@@ -14,6 +14,17 @@ interface IDxTemplate {
     render: (data: IDxTemplateData) => any;
 }
 
+interface ITemplateInfo {
+    name: string;
+    prop: string;
+    isNested: boolean;
+    isComponent: boolean;
+}
+
+interface IWrappedTemplateInfo extends ITemplateInfo {
+    createWrapper: (contentProvider: (model: object) => any) => any;
+}
+
 class TemplateHelper {
     private readonly _component: Component<any>;
 
@@ -24,28 +35,28 @@ class TemplateHelper {
         this._updateState = this._updateState.bind(this);
     }
 
-    public wrapTemplate(component: any, render: any): IDxTemplate {
-        const tmplFn = !!component
-            ? React.createElement.bind(this, component)
-            : render.bind(this);
+    public getContentProvider(templateSource: any, isComponent: boolean)  {
+        return isComponent ? React.createElement.bind(this, templateSource) : templateSource.bind(this);
+    }
 
+    public wrapTemplate(templateInfo: ITemplateInfo): IDxTemplate {
         return {
             render: (data: IDxTemplateData) => {
                 const templateId = "__template_" + generateID();
-                const wrapper = () =>
+                const createWrapper = (contentProvider) =>
                     React.createElement<ITemplateWrapperProps>(TemplateWrapper, {
-                        content: tmplFn(data.model),
+                        content: contentProvider(data.model),
                         container: unwrapElement(data.container),
                         onRemoved: () => this._updateState((t) => delete t[templateId]),
                         key: templateId
                     });
 
-                this._updateState((t) => t[templateId] = wrapper);
+                this._updateState((t) => t[templateId] = { ...templateInfo, createWrapper });
             }
         };
     }
 
-    private _updateState(callback: (templates: Record<string, any>) => void) {
+    private _updateState(callback: (templates: Record<string, IWrappedTemplateInfo>) => void) {
         this._component.setState((state: IState) => {
             const templates = { ...state.templates };
             callback(templates);
@@ -63,5 +74,7 @@ function unwrapElement(element: any) {
 export {
     IDxTemplate,
     IDxTemplateData,
+    ITemplateInfo,
+    IWrappedTemplateInfo,
     TemplateHelper
 };
diff --git a/tools/src/component-generator.ts b/tools/src/component-generator.ts
index eab45bb..945e022 100644
--- a/tools/src/component-generator.ts
+++ b/tools/src/component-generator.ts
@@ -1,5 +1,16 @@
-import { createKeyComparator, isNotEmptyArray, lowercaseFirst, uppercaseFirst } from "./helpers";
-import createTempate from "./template";
+import {
+    createKeyComparator,
+    isNotEmptyArray,
+    lowercaseFirst,
+    uppercaseFirst
+} from "./helpers";
+
+import {
+    createStrictTemplate,
+    createTempate,
+    NEWLINE as NL,
+    TAB
+} from "./template";
 
 interface IComponent {
     name: string;
@@ -39,12 +50,21 @@ interface IRenderedPropTyping {
 }
 
 function generate(component: IComponent): string {
+
     const templates = component.templates
-        ? component.templates.map(createTemplateModel)
+        ? component.templates.map((actualOptionName) => ({
+            actualOptionName,
+            render: formatTemplatePropName(actualOptionName, "Render"),
+            component: formatTemplatePropName(actualOptionName, "Component")
+        }))
         : null;
 
-    const subscribableOptions = component.subscribableOptions
-        ? component.subscribableOptions.map(createSubscribableOptionModel)
+    const defaultProps = component.subscribableOptions
+        ? component.subscribableOptions.map((o) => ({
+            name: `default${uppercaseFirst(o.name)}`,
+            type: o.type,
+            actualOptionName: o.name
+        }))
         : null;
 
     const renderedPropTypings = component.propTypings
@@ -78,8 +98,7 @@ function generate(component: IComponent): string {
                     className: c.className,
                     optionName: c.optionName,
                     ownerName: c.owner,
-                    interfaceName: `I${uppercaseFirst(c.optionName)}Options`,
-                    rendered: renderObject(options, 0),
+                    renderedType: renderObject(options, 0),
                     renderedSubscribableOptions,
                     isCollectionItem: c.isCollectionItem
                 };
@@ -94,47 +113,51 @@ function generate(component: IComponent): string {
         });
     }
 
-    return renderComponent({
-        className: component.name,
-        baseComponentPath: component.baseComponentPath,
-        configComponentPath: component.configComponentPath,
-        dxExportPath: component.dxExportPath,
-        widgetName: `dx${uppercaseFirst(component.name)}`,
-        optionsName,
-        hasExtraOptions: !!templates || !!subscribableOptions,
-        subscribableOptions,
-        templates,
-        nestedComponents,
-        renderedPropTypings,
+    const hasExtraOptions = !!templates || !!defaultProps;
+    const widgetName =  `dx${uppercaseFirst(component.name)}`;
+
+    return renderModule({
+
+        renderedImports: renderImports({
+            dxExportPath: component.dxExportPath,
+            baseComponentPath: component.baseComponentPath,
+            configComponentPath: component.configComponentPath,
+            widgetName,
+            optionsAliasName: hasExtraOptions ? undefined : optionsName,
+            hasExtraOptions,
+            hasNestedComponents: isNotEmptyArray(nestedComponents),
+            hasPropTypings: isNotEmptyArray(renderedPropTypings)
+        }),
+
+        renderedOptionsInterface: !hasExtraOptions ? undefined : renderOptionsInterface({
+            optionsName,
+            defaultProps,
+            templates
+        }),
+
+        renderedComponent: renderComponent({
+            className: component.name,
+            widgetName,
+            optionsName,
+            renderedTemplateProps: templates && templates.map(renderTemplateOption),
+            renderedDefaultProps: defaultProps && defaultProps.map((o) =>
+                renderObjectEntry({
+                    key: o.name,
+                    value: o.actualOptionName
+                })
+            ),
+            renderedPropTypings,
+        }),
+
+        renderedNestedComponents: nestedComponents && nestedComponents.map(renderNestedComponent),
         renderedExports: renderExports(exportNames)
     });
 }
 
-function createTemplateModel(name: string) {
-    const model = {
-        render: formatTemplatePropName(name, "Render"),
-        component: formatTemplatePropName(name, "Component"),
-    };
-
-    return { ...model, renderedProp: renderTemplateOption({ name, ...model }) };
-}
-
 function formatTemplatePropName(name: string, suffix: string): string {
     return lowercaseFirst(name.replace(/template$/i, suffix));
 }
 
-function createSubscribableOptionModel(option: IOption) {
-    const name = `default${uppercaseFirst(option.name)}`;
-    return {
-        name,
-        type: option.type,
-        renderedProp: renderObjectEntry({
-            key: name,
-            value: option.name
-        })
-    };
-}
-
 function createPropTypingModel(typing: IPropTyping): IRenderedPropTyping {
     const types = typing.types.map((t) => "PropTypes." + t);
     if (isNotEmptyArray(typing.acceptableValues)) {
@@ -147,100 +170,161 @@ function createPropTypingModel(typing: IPropTyping): IRenderedPropTyping {
 }
 
 // tslint:disable:max-line-length
-const renderComponent: (model: {
-    className: string;
-    widgetName: string;
-    optionsName: string;
-    baseComponentPath: string;
-    configComponentPath: string;
+const renderModule: (model: {
+    renderedImports: string;
+    renderedOptionsInterface: string;
+    renderedComponent: string;
+    renderedNestedComponents: string[];
+    renderedExports: string;
+}) => string = createTempate(
+`<#= it.renderedImports #>` + `\n` +
+
+`<#? it.renderedOptionsInterface #>` +
+    `<#= it.renderedOptionsInterface #>` + `\n` + `\n` +
+`<#?#>` +
+
+`<#= it.renderedComponent #>` +
+
+`<#? it.renderedNestedComponents #>` +
+    `// tslint:disable:max-classes-per-file` +
+    `<#~ it.renderedNestedComponents :nestedComponent #>` + `\n` + `\n` +
+        `<#= nestedComponent #>` +
+    `}<#~#>` + `\n` + `\n` +
+`<#?#>` +
+
+`export {
+<#= it.renderedExports #>
+};
+`);
+// tslint:enable:max-line-length
+
+const renderImports: (model: {
     dxExportPath: string;
+    configComponentPath: string;
+    baseComponentPath: string;
+    widgetName: string;
+    optionsAliasName: string;
     hasExtraOptions: boolean;
+    hasPropTypings: boolean;
+    hasNestedComponents: boolean;
+}) => string = createTempate(
+`import <#= it.widgetName #>, {
+    IOptions` + `<#? it.optionsAliasName #> as <#= it.optionsAliasName #><#?#>` + `\n` +
+`} from "devextreme/<#= it.dxExportPath #>";` + `\n` + `\n` +
+
+`<#? it.hasPropTypings #>` +
+    `import { PropTypes } from "prop-types";` + `\n` +
+`<#?#>` +
+
+`import BaseComponent from "<#= it.baseComponentPath #>";` + `\n` +
+
+`<#? it.hasNestedComponents #>` +
+    `import NestedOption from "<#= it.configComponentPath #>";` + `\n` +
+`<#?#>`
+);
+
+const renderOptionsInterface: (model: {
+    optionsName: string;
     templates: Array<{
         render: string;
         component: string;
-        renderedProp: string;
     }>;
-    subscribableOptions: Array<{
+    defaultProps: Array<{
         name: string;
         type: string;
-        renderedProp: string;
-    }>;
-    nestedComponents: Array<{
-        optionName: string;
-        className: string;
-        ownerName: string;
-        interfaceName: string;
-        rendered: string;
-        renderedSubscribableOptions: string;
-        isCollectionItem: boolean;
     }>;
-    renderedPropTypings: string[],
-    renderedExports: string
-}
-) => string = createTempate(`
-import <#= it.widgetName #>, {
-    IOptions<#? !it.hasExtraOptions #> as <#= it.optionsName #><#?#>
-} from "devextreme/<#= it.dxExportPath #>";
-
-<#? it.renderedPropTypings #>import { PropTypes } from "prop-types";
-<#?#>import BaseComponent from "<#= it.baseComponentPath #>";
-<#? it.nestedComponents #>import NestedOption from "<#= it.configComponentPath #>";
-<#?#><#? it.hasExtraOptions #>
-interface <#= it.optionsName #> extends IOptions {<#~ it.templates :template #>
-  <#= template.render #>?: (props: any) => React.ReactNode;
-  <#= template.component #>?: React.ComponentType<any>;<#~#><#~ it.subscribableOptions :option #>
-  <#= option.name #>?: <#= option.type #>;<#~#>
-}
-<#?#>
-class <#= it.className #> extends BaseComponent<<#= it.optionsName #>> {
+}) => string = createTempate(
+`interface <#= it.optionsName #> extends IOptions {` + `\n` +
+
+`<#~ it.templates :template #>` +
+    `  <#= template.render #>?: (props: any) => React.ReactNode;` + `\n` +
+    `  <#= template.component #>?: React.ComponentType<any>;` + `\n` +
+`<#~#>` +
+
+`<#~ it.defaultProps :prop #>` +
+    `  <#= prop.name #>?: <#= prop.type #>;` + `\n` +
+`<#~#>` +
+
+`}`
+);
+
+const renderComponent: (model: {
+    className: string;
+    widgetName: string;
+    optionsName: string;
+    renderedDefaultProps: string[];
+    renderedTemplateProps: string[];
+    renderedPropTypings: string[];
+}) => string = createTempate(
+`class <#= it.className #> extends BaseComponent<<#= it.optionsName #>> {
 
   public get instance(): <#= it.widgetName #> {
     return this._instance;
   }
 
   protected _WidgetClass = <#= it.widgetName #>;
-<#? it.subscribableOptions #>
-  protected _defaults = {<#= it.subscribableOptions.map(t => t.renderedProp).join(',') #>
+<#? it.renderedDefaultProps #>
+  protected _defaults = {<#= it.renderedDefaultProps.join(',') #>
   };
-<#?#><#? it.templates #>
-  protected _templateProps = [<#= it.templates.map(t => t.renderedProp).join(', ') #>];
-<#?#>}<#? it.renderedPropTypings #>
-(<#= it.className #> as any).propTypes = {<#= it.renderedPropTypings.join(',') #>
-};<#?#><#? it.nestedComponents #>
-// tslint:disable:max-classes-per-file<#~ it.nestedComponents :nested #>
-
-class <#= nested.className #> extends NestedOption<<#= nested.rendered #>> {<#? nested.isCollectionItem #>
-  public static IsCollectionItem = true;<#?#>
-  public static OwnerType = <#= nested.ownerName #>;
-  public static OptionName = "<#= nested.optionName #>";<#? nested.renderedSubscribableOptions #>
-  public static DefaultsProps = {<#= nested.renderedSubscribableOptions.join(',') #>
-  };<#?#>
-}<#~#>
-<#?#>
-export {
-<#= it.renderedExports #>
-};
-`.trimLeft());
-// tslint:enable:max-line-length
+<#?#><#? it.renderedTemplateProps #>
+  protected _templateProps = [<#= it.renderedTemplateProps.join(', ') #>];
+<#?#>}` + `\n` +
+
+`<#? it.renderedPropTypings #>` +
+    `(<#= it.className #> as any).propTypes = {` + `\n` +
+        `<#= it.renderedPropTypings.join(',\\n') #>` + `\n` +
+    `};` + `\n` +
+`<#?#>`
+);
+
+const renderNestedComponent: (model: {
+    className: string;
+    ownerName: string;
+    optionName: string;
+    isCollectionItem: boolean;
+    renderedType: string;
+    renderedSubscribableOptions: string[];
+}) => string = createTempate(
+`class <#= it.className #> extends NestedOption<<#= it.renderedType #>> {` + `\n` +
+`  public static OwnerType = <#= it.ownerName #>;` + `\n` +
+`  public static OptionName = "<#= it.optionName #>";` + `\n` +
+
+`<#? it.isCollectionItem #>` +
+`  public static IsCollectionItem = true;` + `\n` +
+`<#?#>` +
+
+`<#? it.renderedSubscribableOptions #>` +
+`  public static DefaultsProps = {<#= it.renderedSubscribableOptions.join(',') #>` + `\n` +
+`  };` + `\n` +
+`<#?#>`
+);
 
 const renderTemplateOption: (model: {
-    name: string;
+    actualOptionName: string;
     render: string;
     component: string;
 }) => string = createTempate(`
   {
-    tmplOption: "<#= it.name #>",
+    tmplOption: "<#= it.actualOptionName #>",
     render: "<#= it.render #>",
     component: "<#= it.component #>"
   }
 `.trim());
 
 // tslint:disable:max-line-length
-const renderPropTyping: (model: IRenderedPropTyping) => string = createTempate(`
-  <#= it.propName #>: <#? it.renderedTypes.length === 1 #><#= it.renderedTypes[0] #><#??#>PropTypes.oneOfType([
-    <#= it.renderedTypes.join(',\\n    ') #>
-  ])<#?#>
-`.trimRight());
+const renderPropTyping: (model: IRenderedPropTyping) => string = createTempate(
+`  <#= it.propName #>: ` +
+
+`<#? it.renderedTypes.length === 1 #>` +
+    `<#= it.renderedTypes[0] #>` +
+
+`<#??#>` +
+
+    `PropTypes.oneOfType([` + `\n` +
+    `    <#= it.renderedTypes.join(',\\n    ') #>` + `\n` +
+    `  ])` +
+`<#?#>`
+);
 // tslint:enable:max-line-length
 
 const renderObjectEntry: (model: {
diff --git a/tools/src/index-generator.ts b/tools/src/index-generator.ts
index 3d1fdca..fb1c055 100644
--- a/tools/src/index-generator.ts
+++ b/tools/src/index-generator.ts
@@ -1,4 +1,4 @@
-import createTempate from "./template";
+import { createTempate } from "./template";
 
 function generate(paths: string[]): string {
   return render(paths);
diff --git a/tools/src/template.ts b/tools/src/template.ts
index fcddcf7..37cf655 100644
--- a/tools/src/template.ts
+++ b/tools/src/template.ts
@@ -1,6 +1,17 @@
 import { template, templateSettings } from "dot";
 
-const settings = {
+interface ISettings {
+    strip: boolean;
+}
+
+const TAB = "<#= this.__tab #>";
+const NEWLINE = "<#= this.__newLine #>";
+const symbols = {
+    __newLine: "\n",
+    __tab: "  "
+};
+
+const defaultSettings: ISettings = {
     ...templateSettings,
     conditional: /\<#\?(\?)?\s*([\s\S]*?)\s*#\>/g,
     define: /\<###\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)##\>/g,
@@ -13,6 +24,17 @@ const settings = {
     varname: "it"
 };
 
-const createTempate = (templateStr: string): ((model: any) => string) => template(templateStr, settings);
+function createTempate(templateStr: string, settings?: ISettings): ((model: any) => string) {
+    return template(templateStr, { ...defaultSettings, ...settings });
+}
 
-export default createTempate;
+function createStrictTemplate(templateStr: string): ((model: any) => string) {
+    return template(templateStr, { ...defaultSettings, strip: true }).bind(symbols);
+}
+
+export {
+    createTempate,
+    createStrictTemplate,
+    TAB,
+    NEWLINE
+};


Test patch:
diff --git a/src/core/component.test.tsx b/src/core/component.test.tsx
index 271c9b1..e545a96 100644
--- a/src/core/component.test.tsx
+++ b/src/core/component.test.tsx
@@ -99,14 +99,19 @@ describe("component rendering", () => {
 });
 
 describe("templates", () => {
-    function renderItemTemplate(model: any, container?: any): Element {
+    function renderTemplate(name: string, model?: any, container?: any): Element {
+        model = model || {};
         container = container || document.createElement("div");
-        const render = WidgetClass.mock.calls[0][1].integrationOptions.templates.item.render;
+        const render = WidgetClass.mock.calls[0][1].integrationOptions.templates[name].render;
         return render({
             container, model
         });
     }
 
+    function renderItemTemplate(model?: any, container?: any): Element {
+        return renderTemplate("item", model, container);
+    }
+
     describe("function template", () => {
 
         it("pass integrationOptions to widget", () => {
@@ -172,6 +177,22 @@ describe("templates", () => {
             component.update();
             expect(component.find(".template").html()).toBe('<div class="template">Template is rendered</div>');
         });
+
+        it("renders new template after component change", () => {
+            const ItemTemplate = () => <div className={"template"}>First Template</div>;
+            const component = mount(
+                <ComponentWithTemplates itemRender={ItemTemplate} />
+            );
+
+            const SecondItemTemplate = () => <div className={"template"}>Second Template</div>;
+            component.setProps({
+                itemRender: SecondItemTemplate
+            });
+
+            renderItemTemplate();
+            component.update();
+            expect(component.find(".template").html()).toBe('<div class="template">Second Template</div>');
+        });
     });
 
     describe("component template", () => {
@@ -215,6 +236,22 @@ describe("templates", () => {
             component.update();
             expect(component.find(".template").html()).toBe('<div class="template">Template is rendered</div>');
         });
+
+        it("renders new template after component change", () => {
+            const ItemTemplate = () => <div className={"template"}>First Template</div>;
+            const component = mount(
+                <ComponentWithTemplates itemComponent={ItemTemplate} />
+            );
+
+            const SecondItemTemplate = () => <div className={"template"}>Second Template</div>;
+            component.setProps({
+                itemComponent: SecondItemTemplate
+            });
+
+            renderItemTemplate();
+            component.update();
+            expect(component.find(".template").html()).toBe('<div class="template">Second Template</div>');
+        });
     });
 
     describe("nested template", () => {
@@ -242,6 +279,38 @@ describe("templates", () => {
             expect(integrationOptions.templates.item2).toBeDefined();
             expect(typeof integrationOptions.templates.item2.render).toBe("function");
         });
+
+        it("renders nested templates", () => {
+            const FirstTemplate = () => <div className={"template"}>Template</div>;
+            const component = mount(
+                <ComponentWithTemplates>
+                    <Template name={"item1"} render={FirstTemplate}/>
+                </ComponentWithTemplates >
+            );
+            renderTemplate("item1");
+            component.update();
+            expect(component.find(".template").html()).toBe('<div class="template">Template</div>');
+        });
+
+        it("renders nested templates", () => {
+            const FirstTemplate = () => <div className={"template"}>First Template</div>;
+            const component = mount(
+                <ComponentWithTemplates>
+                    <Template name={"item1"} render={FirstTemplate}/>
+                </ComponentWithTemplates >
+            );
+            renderTemplate("item1");
+            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: <Template name={"item1"} render={SecondTemplate}/>
+            });
+            component.update();
+            expect(component.find(".template").html()).toBe('<div class="template">Second Template</div>');
+        });
+
     });
 
     it("pass component option changes to widget", () => {
@@ -283,7 +352,7 @@ describe("templates", () => {
         const component = mount(
             <ComponentWithTemplates itemComponent={ItemTemplate} />
         );
-        renderItemTemplate({});
+        renderItemTemplate();
         expect(Object.getOwnPropertyNames(component.state("templates")).length).toBe(1);
         component.update();
         events.triggerHandler(component.find(".template").getDOMNode().parentNode, "dxremove");
diff --git a/tools/src/component-generator.test.ts b/tools/src/component-generator.test.ts
index 38fca8a..d4cd5f7 100644
--- a/tools/src/component-generator.test.ts
+++ b/tools/src/component-generator.test.ts
@@ -400,9 +400,9 @@ class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {
 class Opt_1_Component extends NestedOption<{
   sub_opt_2?: TYPE_1;
 }> {
-  public static IsCollectionItem = true;
   public static OwnerType = CLASS_NAME;
   public static OptionName = "opt_1";
+  public static IsCollectionItem = true;
 }
 
 class Opt_6_SubComponent extends NestedOption<{