Repository: DevExpress/devextreme-vue
Pull request #264 (2019-04-26 15:08:14)

Fix render of template of nested components
Cherry-pick of #258 


Patch:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index b3f05c3..4d3a689 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,5 @@
 {
     "editor.tabSize": 4,
-    "editor.insertSpaces": true
+    "editor.insertSpaces": true,
+    "typescript.tsdk": "node_modules\\typescript\\lib"
 }
\ No newline at end of file
diff --git a/example/app.vue b/example/app.vue
index 2829401..ba6a64e 100644
--- a/example/app.vue
+++ b/example/app.vue
@@ -5,6 +5,9 @@
         <popup-example/>
         <br/>
         <text-box-example/>
+        <br/>
+        <menu-example/>
+        <br/>
         <number-box-example/>
         <br />
         <validation-example/>
@@ -38,6 +41,7 @@ import GridExample from "./components/grid-example";
 import PopupExample from "./components/popup-example";
 import TextBoxExample from "./components/text-box-example";
 import MapExample from "./components/map-example";
+import MenuExample from "./components/menu-example";
 import NumberBoxExample from "./components/number-box-example";
 import ValidationExample from "./components/validation-example";
 import SchedulerExample from "./components/scheduler-example";
@@ -53,6 +57,7 @@ export default {
         PopupExample,
         TextBoxExample,
         MapExample,
+        MenuExample,
         NumberBoxExample,
         ValidationExample,
         ListExample,
diff --git a/example/components/menu-example.vue b/example/components/menu-example.vue
new file mode 100644
index 0000000..7348937
--- /dev/null
+++ b/example/components/menu-example.vue
@@ -0,0 +1,54 @@
+<template>
+    <example-block title="dxMenu">
+        <h4>Menu with simple items</h4>
+        <dx-menu>
+            <dx-item text="Item 1">
+                <dx-item text="Item 1-1"/>
+                <dx-item text="Item 1-2"/>
+                <dx-item text="Item 1-3">
+                    <dx-item text="Item 1-3-1"/>
+                </dx-item>
+            </dx-item>
+            <dx-item text="Item 2"/>
+            <dx-item text="Item 3"/>
+        </dx-menu>
+
+        <h4>Menu with static item templates</h4>
+        <dx-menu>
+            <dx-item>
+                <div slot-scope="_">Item 1</div>
+                <dx-item>
+                    <div slot-scope="_">Item 1-1</div>
+                </dx-item>
+                <dx-item>
+                    <div slot-scope="_">Item 1-2</div>
+                </dx-item>
+                <dx-item>
+                    <div slot-scope="_">Item 1-3</div>
+                    <dx-item>
+                        <div slot-scope="_">Item 1-3-1</div>
+                    </dx-item>
+                </dx-item>
+            </dx-item>
+            <dx-item>
+                <div slot-scope="_">Item 2</div>
+            </dx-item>
+            <dx-item>
+                <div slot-scope="_">Item 3</div>
+            </dx-item>
+        </dx-menu>
+    </example-block>
+</template>
+
+<script>
+import ExampleBlock from "./example-block";
+import DxMenu, { DxItem } from "../../src/menu";
+
+export default {
+  components: {
+    ExampleBlock,
+    DxMenu,
+    DxItem
+  }
+};
+</script>
diff --git a/src/core/configuration.ts b/src/core/configuration.ts
index c765165..eb24a3d 100644
--- a/src/core/configuration.ts
+++ b/src/core/configuration.ts
@@ -16,6 +16,7 @@ class Configuration {
     private readonly _initialValues: Record<string, any>;
     private readonly _expectedChildren: Record<string, ExpectedChild>;
     private readonly _updateFunc: UpdateFunc;
+    private readonly _ownerConfig: Pick<Configuration, "fullPath"> | undefined;
     private _nestedConfigurations: Configuration[];
     private _optionChangedFunc: any;
 
@@ -28,6 +29,7 @@ class Configuration {
         expectedChildren?: Record<string, ExpectedChild>,
         isCollectionItem?: boolean,
         collectionItemIndex?: number,
+        ownerConfig?: Pick<Configuration, "fullPath"> | undefined
     ) {
         this._updateFunc = updateFunc;
         this._name = name;
@@ -36,6 +38,7 @@ class Configuration {
         this._isCollectionItem = !!isCollectionItem;
         this._collectionItemIndex = collectionItemIndex;
         this._expectedChildren = expectedChildren || {};
+        this._ownerConfig = ownerConfig;
 
         this.updateValue = this.updateValue.bind(this);
     }
@@ -45,7 +48,17 @@ class Configuration {
     }
 
     public get fullPath(): string | null {
-        return this._isCollectionItem ? `${this._name}[${this._collectionItemIndex}]` : this._name;
+        let path = this._name;
+
+        if (this._ownerConfig && this._ownerConfig.fullPath) {
+            path = `${this._ownerConfig.fullPath}.${path}`;
+        }
+
+        if (this._isCollectionItem) {
+            path = `${path}[${this._collectionItemIndex}]`;
+        }
+
+        return path;
     }
 
     public get options(): string[] {
@@ -120,12 +133,13 @@ class Configuration {
         }
 
         const configuration = new Configuration(
-            this.updateValue,
+            this._updateFunc,
             actualName,
             initialValues,
             expectedChildren,
             actualIsCollectionItem,
-            collectionItemIndex
+            collectionItemIndex,
+            this
         );
 
         this._nestedConfigurations.push(configuration);
@@ -162,7 +176,7 @@ class Configuration {
             }
         });
 
-        return Object.keys(values).length > 0 ? values : undefined;
+        return values;
     }
 
     public getOptionsToWatch(): string[] {


Test patch:
diff --git a/src/core/component.test.ts b/src/core/component.test.ts
index 22962e9..b4ad0e5 100644
--- a/src/core/component.test.ts
+++ b/src/core/component.test.ts
@@ -1093,6 +1093,39 @@ describe("static items", () => {
         expect(renderedTemplate.innerHTML).toBe("1");
     });
 
+    it("render nested template", () => {
+        const NestedItem = Vue.extend({
+            extends: DxConfiguration,
+            props: {
+                prop1: Number,
+                template: String
+            }
+        });
+        (NestedItem as any as IConfigurationComponent).$_optionName = "items";
+        (NestedItem as any as IConfigurationComponent).$_isCollectionItem = true;
+
+        new Vue({
+            template: `<test-component>
+                         <nested-item>
+                            <div slot-scope="_">1</div>
+                            <nested-item>
+                                <div slot-scope="_">2</div>
+                            </nested-item>
+                         </nested-item>
+                       </test-component>`,
+            components: {
+                TestComponent,
+                NestedItem
+            }
+        }).$mount();
+
+        const renderedTemplate = renderTemplate("items[0].template");
+        const renderedNestedTemplate = renderTemplate("items[0].items[0].template");
+
+        expect(renderedTemplate.innerHTML).toBe("1");
+        expect(renderedNestedTemplate.innerHTML).toBe("2");
+    });
+
     it("doesn't pass integrationOptions to widget if nestd item has sub nested item", () => {
         const NestedItem = Vue.extend({
             extends: DxConfiguration,
diff --git a/src/core/configuration.test.ts b/src/core/configuration.test.ts
index 864d1a9..b6c3ad8 100644
--- a/src/core/configuration.test.ts
+++ b/src/core/configuration.test.ts
@@ -13,6 +13,63 @@ function createConfigWithExpectedChildren(children: Record<string, ExpectedChild
     );
 }
 
+describe("fullPath building", () => {
+
+    const testCases: Array<{
+        msg: string;
+        name: string | null;
+        ownerPath?: string;
+        expected: string | null;
+        collectionIndex?: number;
+    }> = [
+            {
+                msg: "works for null",
+                name: null,
+                expected: null
+            },
+            {
+                msg: "works without owner",
+                name: "abc",
+                expected: "abc"
+            },
+            {
+                msg: "works with owner",
+                name: "abc",
+                ownerPath: "def",
+                expected: "def.abc"
+            },
+            {
+                msg: "works for collection item",
+                name: "abc",
+                collectionIndex: 123,
+                expected: "abc[123]"
+            },
+            {
+                msg: "works for collection item with owner",
+                name: "abc",
+                ownerPath: "def",
+                collectionIndex: 123,
+                expected: "def.abc[123]"
+            }
+        ];
+
+    for (const { msg, name, collectionIndex, ownerPath, expected } of testCases) {
+        it(msg, () => {
+            const isCollection = collectionIndex !== undefined;
+            const ownerConfig = ownerPath ? { fullPath: ownerPath } : undefined;
+            expect(new Configuration(
+                jest.fn(),
+                name,
+                {},
+                undefined,
+                isCollection,
+                collectionIndex,
+                ownerConfig
+            ).fullPath).toBe(expected);
+        });
+    }
+});
+
 it("calls update from nested", () => {
     const callback = jest.fn();
     const root = createRootConfig(callback);
@@ -266,13 +323,13 @@ describe("initial configuration", () => {
         });
     });
 
-    it("pulls undefined if no values provided", () => {
+    it("pulls empty value for correct option structure T728446", () => {
         const root = createRootConfig(jest.fn());
 
-        const nested = root.createNested("option", {});
+        const nested = root.createNested("option", {}, true);
         nested.createNested("subOption", {});
 
-        expect(root.getInitialValues()).toBeUndefined();
+        expect(root.getInitialValues()).toMatchObject({ option: [{ subOption: {} }]});
     });
 
     it("pulls values and ignores empty nested", () => {