Repository: DevExpress/devextreme-react (js)

Issue:
Form: Tab render prop not working
<!--
  Note: if the issue is a general React question, please check React Docs first: https://reactjs.org/docs
  If your question is about  DevExtreme widgets' APIs, use the DevExpress Support Center: https://www.devexpress.com/Support/Center
-->

### Issue Type
Bug Report

### Description
Form: Tab render prop not taken into account

We have a Devextreme-React form, using tabs to split it into section (Form/TabbedItem).
At some places, we have to render custom React components. Tabs and GroupItem provide render props that unfortunately do not seem to be taken into account, or sometime work in surprising ways.

The same things using template (and JQuery) props seem to work fine.

#### Steps to Reproduce
Create a Form with a Nested TabbedItem. Inside the TabbedItem
- create a Tab and use the render/tabRender/component/tabComponent props to render custom content.
- create a GroupItem inside a Tab and use the render/component props to render custom content.

#### Current Behavior
Custom content is not rendered; the render props does not seem to be ever called (that can be checked with a breakpoint).

More surprising results can be observed when specifying multiple GroupItem inside a Tab; some GroupItems seem to escape the tab (cf. the demo below)

#### Expected Behavior
The component should call the render function and render the custom component.

#### Demo
Please see https://stackblitz.com/edit/react-aukapj

### Environment Details
<!--
Specify DevExtreme version, DevExtreme React Components version, React version and other environment details or notes you consider important.
-->
- devextreme version: 18.2.4
- devextreme-react version: 18.2.4
- react version: 16.5.0


Pull request description:
Fix pulling templates from nested config-components
Fixes #285 
Cherry-pick of #289 

Patch:
diff --git a/src/core/options-manager.ts b/src/core/options-manager.ts
index 8885a12..89f156a 100644
--- a/src/core/options-manager.ts
+++ b/src/core/options-manager.ts
@@ -11,6 +11,7 @@ interface INestedOption {
 }
 
 interface INestedConfigDescr extends INestedOption {
+    optionFullName: string;
     defaults: Record<string, any>;
     templates: ITemplateMeta[];
     elementEntries: Array<{
@@ -223,9 +224,9 @@ class OptionsManager {
                     },
                     props: props.templates,
                     templateProps: configComponent.templates,
-                    ownerName: this.buildFullOptionName(
-                        configComponent.optionName,
-                        configComponent.isCollectionItem ? index : undefined
+                    ownerName: this.buildOptionItemName(
+                        configComponent,
+                        configComponent.isCollectionItem ? index : undefined,
                     ),
                     propsGetter: (prop) => configComponent.elementEntries[index].element.props[prop]
                 });
@@ -265,18 +266,23 @@ class OptionsManager {
         const optionName = resolvedNested.optionName;
         const isCollectionItem = resolvedNested.isCollectionItem;
 
+        let optionFullName = optionName;
+        if (ownerFullName) {
+            optionFullName = `${ownerFullName}.${optionFullName}`;
+        }
+
         const option = ensureNestedOption(
             optionName,
+            optionFullName,
             owningCollection,
             nestedOptionClass.type.DefaultsProps,
             nestedOptionClass.type.TemplateProps,
             isCollectionItem
         );
 
-        const optionFullName = this.buildFullOptionName(
-            optionName,
-            isCollectionItem ? option.elementEntries.length : undefined,
-            ownerFullName
+        const optionItemName = this.buildOptionItemName(
+            option,
+            isCollectionItem ? option.elementEntries.length : undefined
         );
 
         const nestedOptionMeta: INestedOptionMeta = {
@@ -286,7 +292,7 @@ class OptionsManager {
                     c,
                     nestedOptionClass.type.ExpectedChildren,
                     nestedOptionsCollection,
-                    optionFullName
+                    optionItemName
                 );
             },
             updateFunc: (newProps, prevProps) => {
@@ -294,8 +300,8 @@ class OptionsManager {
                     nestedOptionClass.type.DefaultsProps,
                     nestedOptionClass.type.TemplateProps).options;
                 this.processChangedValues(
-                    addPrefixToKeys(newOptions, optionFullName + "."),
-                    addPrefixToKeys(prevProps, optionFullName + ".")
+                    addPrefixToKeys(newOptions, optionItemName + "."),
+                    addPrefixToKeys(prevProps, optionItemName + ".")
                 );
             },
             makeDirty: () => {
@@ -316,18 +322,14 @@ class OptionsManager {
         return optionComponent;
     }
 
-    private buildFullOptionName(optionName: string, index?: number, ownerName?: string, ): string {
-        let optionFullName = optionName;
+    private buildOptionItemName(descr: INestedConfigDescr, index?: number): string {
+        let optionItemName = descr.optionFullName;
 
         if (index !== undefined) {
-            optionFullName += `[${index}]`;
-        }
-
-        if (ownerName) {
-            optionFullName = `${ownerName}.${optionFullName}`;
+            optionItemName += `[${index}]`;
         }
 
-        return optionFullName;
+        return optionItemName;
     }
 
     private _setGuard(optionName: string, optionValue: any): void {
@@ -347,6 +349,7 @@ class OptionsManager {
 
 function ensureNestedOption(
     optionName: string,
+    optionFullName: string,
     optionsCollection: Record<string, INestedConfigDescr>,
     defaults: Record<string, any>,
     templates: ITemplateMeta[],
@@ -358,6 +361,7 @@ function ensureNestedOption(
     ) {
         optionsCollection[optionName] = {
             optionName,
+            optionFullName,
             defaults,
             templates,
             elementEntries: [],


Test patch:
diff --git a/src/core/__tests__/template.test.tsx b/src/core/__tests__/template.test.tsx
index 2bb37fd..6435ab4 100644
--- a/src/core/__tests__/template.test.tsx
+++ b/src/core/__tests__/template.test.tsx
@@ -469,6 +469,12 @@ describe("component/render in nested options", () => {
             <TestComponent>
                 <CollectionNestedComponent render={UserTemplate} />
                 <CollectionNestedComponent render={UserTemplate} />
+                <CollectionNestedComponent>
+                    <NestedComponent itemRender={UserTemplate}/>
+                </CollectionNestedComponent>
+                <NestedComponent>
+                    <CollectionNestedComponent render={UserTemplate} />
+                </NestedComponent>
             </TestComponent>
         );
 
@@ -476,10 +482,17 @@ describe("component/render in nested options", () => {
 
         expect(options["collection[0].template"]).toBe("collection[0].template");
         expect(options["collection[1].template"]).toBe("collection[1].template");
+        expect(options["collection[2].option.item"]).toBe("collection[2].option.item");
+        expect(options["option.collection[0].template"]).toBe("option.collection[0].template");
 
         const integrationOptions = options.integrationOptions;
 
-        expect(Object.keys(integrationOptions.templates)).toEqual(["collection[0].template", "collection[1].template"]);
+        expect(Object.keys(integrationOptions.templates)).toEqual([
+            "collection[0].template",
+            "collection[1].template",
+            "collection[2].option.item",
+            "option.collection[0].template"
+        ]);
     });
 
     it("pass integrationOptions for collection nested component with 'template' option if a child defined", () => {