Repository: DevExpress/devextreme-vue
Pull request #259 (2019-04-22 15:30:40)

Provide an index for the item templates
Fixes [T726496](https://www.devexpress.com/Support/Center/Question/Details/T726496/there-is-no-way-to-access-the-item-index-in-a-template-in-a-collection-widget).
Config option `useLegacyTemplatEngine` will be set to `false` after all the depended code such as Demos will be prepared.


Patch:
diff --git a/example/app.vue b/example/app.vue
index 12c9d28..a16cad0 100644
--- a/example/app.vue
+++ b/example/app.vue
@@ -46,6 +46,8 @@ import ChartExample from "./components/chart-example";
 import TabPanelExample from "./components/tab-panel-example";
 import FormExample from "./components/form-example";
 
+import config from "../src/core/config";
+config({ useLegacyTemplateEngine: false });
 
 export default {
     components: {
diff --git a/example/components/form-example.vue b/example/components/form-example.vue
index 5bf0c7d..7262bec 100644
--- a/example/components/form-example.vue
+++ b/example/components/form-example.vue
@@ -29,7 +29,7 @@
             :colSpan="2"
         >
             <dx-text-area
-                slot-scope="data"
+                slot-scope="{ data }"
                 :minHeight="80"
                 :value="data.editorOptions.value"
             />
diff --git a/example/components/grid-example.vue b/example/components/grid-example.vue
index 1d15ea5..48a4094 100644
--- a/example/components/grid-example.vue
+++ b/example/components/grid-example.vue
@@ -73,7 +73,7 @@
         <dx-pager :visible="true" :showPageSizeSelector="true" />
         <dx-paging :pageSize="10"/>
 
-        <dx-button slot="cell-city" slot-scope="data" :text="data.text" />
+        <dx-button slot="cell-city" slot-scope="{ data }" :text="data.text" />
       </dx-data-grid>
     </example-block>
 </template>
diff --git a/example/components/list-example.vue b/example/components/list-example.vue
index 3d800ff..92b4905 100644
--- a/example/components/list-example.vue
+++ b/example/components/list-example.vue
@@ -8,11 +8,11 @@
 
         <h4>List with item template</h4>
         <dx-list :items="listData">
-            <div slot="item" slot-scope="data">
-                <i>{{data.day}}</i>
+            <div slot="item" slot-scope="{ data, index }">
+                {{index + 1}} - <i>{{data.day}}</i>
             </div>
-            <div slot="weekend" slot-scope="data">
-                <i>{{data.day}}</i>
+            <div slot="weekend" slot-scope="{ data, index }">
+                {{index + 1}} - <b>{{data.day}}</b>
             </div>
         </dx-list>
         <br/>
@@ -35,10 +35,10 @@
           itemTemplate="weekday"
           :items="listData"
         >
-            <div slot="weekday" slot-scope="data">
+            <div slot="weekday" slot-scope="{ data }">
                 
{{data.day}}

             </div>
-            <div slot="weekend" slot-scope="data">
+            <div slot="weekend" slot-scope="{ data }">
                 <b>{{data.day}}</b>
             </div>
         </dx-list>
diff --git a/src/core/component.ts b/src/core/component.ts
index 532fd5e..3bb2c1f 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -4,6 +4,7 @@ import IVue, { VNode, VueConstructor } from "vue";
 import * as events from "devextreme/events";
 
 import { pullAllChildren } from "./children-processing";
+import { getOption } from "./config";
 import Configuration, { bindOptionWatchers, subscribeOnUpdates } from "./configuration";
 import { IConfigurable } from "./configuration-component";
 import { IExtension, IExtensionComponentNode } from "./extension-component";
@@ -191,6 +192,9 @@ const BaseComponent: VueConstructor<IBaseComponent> = Vue.extend({
         $_fillTemplate(template: any, name: string): object {
             return {
                 render: (data: any) => {
+                    const scope = getOption("useLegacyTemplateEngine")
+                        ? data.model
+                        : { data: data.model, index: data.index };
                     const vm = new Vue({
                         name,
                         inject: ["eventBus"],
@@ -200,7 +204,7 @@ const BaseComponent: VueConstructor<IBaseComponent> = Vue.extend({
                                 this.$forceUpdate();
                             });
                         },
-                        render: () => template(data.model)
+                        render: () => template(scope)
                     }).$mount();
 
                     const element = vm.$el;
diff --git a/src/core/config.ts b/src/core/config.ts
new file mode 100644
index 0000000..761f007
--- /dev/null
+++ b/src/core/config.ts
@@ -0,0 +1,18 @@
+interface IOptions {
+    useLegacyTemplateEngine: boolean;
+  }
+
+let config: IOptions = {
+    useLegacyTemplateEngine: true
+};
+
+function setOptions(options: Partial<IOptions>): void {
+    config = {...config, ... options};
+}
+
+function getOption<TName extends keyof IOptions>(optionName: TName): IOptions[TName] {
+    return config[optionName];
+}
+
+export default setOptions;
+export { getOption };


Test patch:
diff --git a/src/core/component.test.ts b/src/core/component.test.ts
index 379916c..c770123 100644
--- a/src/core/component.test.ts
+++ b/src/core/component.test.ts
@@ -1,10 +1,13 @@
 import Vue, { VueConstructor } from "vue";
 import { DxComponent, IWidgetComponent } from "../core/component";
+import globalConfig from "../core/config";
 import { DxConfiguration, IConfigurable, IConfigurationComponent } from "../core/configuration-component";
 import { DxExtensionComponent } from "../core/extension-component";
 
 import * as events from "devextreme/events";
 
+globalConfig({ useLegacyTemplateEngine: false });
+
 const eventHandlers: { [index: string]: (e?: any) => void } = {};
 const Widget = {
     option: jest.fn(),
@@ -829,13 +832,14 @@ describe("nested option", () => {
 
 });
 
-function renderTemplate(name: string, model?: object, container?: any): Element {
+function renderTemplate(name: string, model?: object, container?: any, index?: number): Element {
     model = model || {};
     container = container || document.createElement("div");
     const render = WidgetClass.mock.calls[0][1].integrationOptions.templates[name].render;
     return render({
         container,
-        model
+        model,
+        index
     });
 }
 
@@ -843,8 +847,8 @@ describe("template", () => {
 
     const DX_TEMPLATE_WRAPPER = "dx-template-wrapper";
 
-    function renderItemTemplate(model?: object, container?: any): Element {
-        return renderTemplate("item", model, container);
+    function renderItemTemplate(model?: object, container?: any, index?: number): Element {
+        return renderTemplate("item", model, container, index);
     }
 
     it("passes integrationOptions to widget", () => {
@@ -891,14 +895,16 @@ describe("template", () => {
     it("renders scoped slot", () => {
         new Vue({
             template: `<test-component>
-                            <div slot='item' slot-scope='props'>Template {{props.text}}</div>
+                            <div slot='item' slot-scope='{ data: { text }, index }'>
+                                Template {{text}} and index {{index}}
+                            </div>
                         </test-component>`,
             components: {
                 TestComponent
             }
         }).$mount();
-        const renderedTemplate = renderItemTemplate({ text: "with data" });
-        expect(renderedTemplate.innerHTML).toBe("Template with data");
+        const renderedTemplate = renderItemTemplate({ text: "with data" }, undefined, 5);
+        expect(renderedTemplate.innerHTML).toContain("Template with data and index 5");
     });
 
     it("adds templates as children", () => {
@@ -943,7 +949,7 @@ describe("template", () => {
     it("unwraps container", () => {
         new Vue({
             template: `<test-component>
-                            <div slot='item' slot-scope='props'>Template {{props.text}}</div>
+                            <div slot='item' slot-scope='{ data }'>Template {{data.text}}</div>
                         </test-component>`,
             components: {
                 TestComponent
diff --git a/src/core/config.test.ts b/src/core/config.test.ts
new file mode 100644
index 0000000..295c233
--- /dev/null
+++ b/src/core/config.test.ts
@@ -0,0 +1,52 @@
+import Vue from "vue";
+import { DxComponent, IWidgetComponent } from "../core/component";
+import config, { getOption } from "../core/config";
+
+const Widget = {
+    option: jest.fn(),
+    dispose: jest.fn(),
+    on: jest.fn(),
+    fire: jest.fn(),
+    beginUpdate: jest.fn(),
+    endUpdate: jest.fn(),
+};
+
+const WidgetClass = jest.fn(() => Widget);
+
+const TestComponent = Vue.extend({
+    extends: DxComponent,
+    beforeCreate() {
+        (this as any as IWidgetComponent).$_WidgetClass = WidgetClass;
+    }
+});
+
+describe("useLegacyTemplateEngine", () => {
+    const originalValue = getOption("useLegacyTemplateEngine");
+
+    beforeEach(() => {
+        config({ useLegacyTemplateEngine: true });
+    });
+
+    afterEach(() => {
+        config({ useLegacyTemplateEngine: originalValue });
+    });
+
+    it("has model as scope", () => {
+        new Vue({
+            template: `<test-component>
+                            <div slot='item' slot-scope='data'>Template {{data.text}}</div>
+                        </test-component>`,
+            components: {
+                TestComponent
+            }
+        }).$mount();
+
+        const render = WidgetClass.mock.calls[0][1].integrationOptions.templates.item.render;
+        const renderedTemplate = render({
+            container: document.createElement("div"),
+            model: { text: "with data" }
+        });
+
+        expect(renderedTemplate.innerHTML).toBe("Template with data");
+    });
+});