Repository: DevExpress/devextreme-vue
Pull request #155 (2018-10-03 14:05:33)

Refactor templates creation


Patch:
diff --git a/example/components/list-example.vue b/example/components/list-example.vue
index 6ca8dc5..0c63e9e 100644
--- a/example/components/list-example.vue
+++ b/example/components/list-example.vue
@@ -3,6 +3,9 @@
         <h4>List of simple items</h4>
         <dx-list :items="simpleData" />
         <br/>
+        <dx-text-box v-model="item"/>
+        <dx-button text="Add" @click="add"/>
+
         <h4>List with item template</h4>
         <dx-list :items="listData">
             <div slot="item" slot-scope="data">
@@ -25,24 +28,41 @@
                 <b>{{data.day}}</b>
             </div>
         </dx-list>
+        <br/>
+        <dx-button text="Toggle Weekend" @click="toggleWeekend"/>
     </example-block>
 </template>
 
 <script>
 import ExampleBlock from "./example-block";
 import { orangesByDay } from "../data";
-import { DxList } from "../../src";
+import { DxButton, DxList, DxTextBox } from "../../src";
 
 export default {
   components: {
     ExampleBlock,
-    DxList
+    DxButton,
+    DxList,
+    DxTextBox
   },
   data: function() {
     return {
+      item: "",
       simpleData: [1, 2, 3, 4],
       listData: orangesByDay
     };
+  },
+  methods: {
+      add() {
+          if (this.item) {
+            this.simpleData.push(this.item)
+            this.item = "";
+          }
+      },
+      toggleWeekend() {
+          this.listData[5].disabled = !this.listData[5].disabled;
+          this.listData[6].disabled = !this.listData[6].disabled;
+      }
   }
 };
 </script>
\ No newline at end of file
diff --git a/example/data.ts b/example/data.ts
index 2417807..ffa1527 100644
--- a/example/data.ts
+++ b/example/data.ts
@@ -1018,10 +1018,12 @@ export const orangesByDay = [{
   day: "Friday",
   oranges: 6
 }, {
+  disabled: false,
   day: "Saturday",
   oranges: 11,
   template: "weekend"
 }, {
+  disabled: false,
   day: "Sunday",
   oranges: 4,
   template: "weekend"
diff --git a/package-lock.json b/package-lock.json
index d86340c..d4b252f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "devextreme-vue",
-  "version": "18.1.5-alpha.8",
+  "version": "18.1.6-alpha.9",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -3821,9 +3821,9 @@
       "dev": true
     },
     "devextreme": {
-      "version": "18.1.5",
-      "resolved": "https://registry.npmjs.org/devextreme/-/devextreme-18.1.5.tgz",
-      "integrity": "sha1-AGqkrxR/tBRIAeLMjlIPiIVdw2Y=",
+      "version": "18.1.6",
+      "resolved": "https://registry.npmjs.org/devextreme/-/devextreme-18.1.6.tgz",
+      "integrity": "sha512-KZUeNkGVXcZqZcyFPKlkA3QFeBHF4Nj6TRV3/SkxGpH9hcAC4x88TOt3REg/SUcfPy4R/rb0rLhKAw/iGOrzXg==",
       "dev": true,
       "requires": {
         "jszip": "^2.0.0 || ^3.0.0"
@@ -4131,7 +4131,7 @@
     },
     "es6-promise": {
       "version": "3.0.2",
-      "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
+      "resolved": "http://registry.npmjs.org/es6-promise/-/es6-promise-3.0.2.tgz",
       "integrity": "sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y=",
       "dev": true
     },
diff --git a/package.json b/package.json
index 495eb98..b94e0ed 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "devextreme-vue",
-  "version": "18.1.5-alpha.8",
+  "version": "18.1.6-alpha.9",
   "description": "DevExtreme Vue UI and Visualization Components",
   "repository": {
     "type": "git",
@@ -27,7 +27,7 @@
   "author": "Developer Express Inc.",
   "license": "MIT",
   "peerDependencies": {
-    "devextreme": "^18.1.5",
+    "devextreme": "^18.1.6",
     "vue": "^2.5.16"
   },
   "devDependencies": {
@@ -35,7 +35,7 @@
     "@types/node": "^9.4.7",
     "css-loader": "^0.28.11",
     "dasherize": "^2.0.0",
-    "devextreme": "^18.1.5",
+    "devextreme": "^18.1.6",
     "dot": "^1.1.2",
     "gulp": "^3.9.1",
     "gulp-clean": "^0.4.0",
diff --git a/src/core/component.ts b/src/core/component.ts
index 36b0f92..c48b7eb 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -5,7 +5,7 @@ import * as events from "devextreme/events";
 
 import Configuration, { bindOptionWatchers, subscribeOnUpdates } from "./configuration";
 import { IConfigurable, IConfigurationComponent } from "./configuration-component";
-import { camelize } from "./helpers";
+import { camelize, toComparable } from "./helpers";
 
 interface IWidgetComponent extends IConfigurable {
     $_instance: any;
@@ -52,7 +52,6 @@ const BaseComponent: VueConstructor = Vue.extend({
                 ...this.$options.propsData,
                 ...config.getInitialValues()
             };
-
             const instance = new (this as any).$_WidgetClass(element, options);
             (this as any as IWidgetComponent).$_instance = instance;
 
@@ -63,20 +62,42 @@ const BaseComponent: VueConstructor = Vue.extend({
         },
 
         $_getIntegrationOptions(): object {
-            if (!this.$scopedSlots || !Object.keys(this.$scopedSlots).length) {
-                return {};
-            }
+            const result: Record<string, any> = {
+                integrationOptions:  {
+                    watchMethod: this.$_getWatchMethod(),
+                }
+            };
 
-            const templates: Record<string, any> = {};
+            if (this.$scopedSlots && Object.keys(this.$scopedSlots).length) {
+                result.integrationOptions.templates = {};
+                Object.keys(this.$scopedSlots).forEach((name: string) => {
+                    result.integrationOptions.templates[name] = this.$_fillTemplate(this.$scopedSlots[name], name);
+                });
+            }
 
-            Object.keys(this.$scopedSlots).forEach((name: string) => {
-                templates[name] = this.$_fillTemplate(this.$scopedSlots[name], name);
-            });
+            return result;
+        },
 
-            return {
-                integrationOptions: {
-                    templates
+        $_getWatchMethod(): (
+            valueGetter: () => any,
+            valueChangeCallback: (value: any) => void,
+            options: { deep: boolean, skipImmediate: boolean }
+        ) => any {
+            return (valueGetter, valueChangeCallback, options) => {
+                options = options || {};
+                if (!options.skipImmediate) {
+                    valueChangeCallback(valueGetter());
                 }
+
+                return this.$watch(() => {
+                    return valueGetter();
+                }, (newValue, oldValue) => {
+                    if (toComparable(oldValue) !== toComparable(newValue) || options.deep) {
+                        valueChangeCallback(newValue);
+                    }
+                }, {
+                    deep: options.deep
+                });
             };
         },
 
diff --git a/src/core/helpers.ts b/src/core/helpers.ts
index 2d35b66..257128a 100644
--- a/src/core/helpers.ts
+++ b/src/core/helpers.ts
@@ -9,3 +9,7 @@ export function lowercaseFirst(value: string): string {
 export function camelize(value: string): string {
     return lowercaseFirst(value.split("-").map((v) => uppercaseFirst(v)).join(""));
 }
+
+export function toComparable(value: any): any {
+    return value instanceof Date ? value.getTime() : value;
+}
diff --git a/tools/src/component-generator.ts b/tools/src/component-generator.ts
index 50be2ae..96d6e5d 100644
--- a/tools/src/component-generator.ts
+++ b/tools/src/component-generator.ts
@@ -1,5 +1,5 @@
 import { compareStrings } from "./helpers";
-import createTempate from "./template";
+import { createTempate, L1, L2, L3, L4, TAB2 } from "./template";
 
 interface IImport {
     name: string;
@@ -107,11 +107,6 @@ function createNestedComponentModel(component: INestedComponent): INestedCompone
 
 // tslint:disable:max-line-length
 
-const L1: string = `\n` + tab(1);
-const L2: string = `\n` + tab(2);
-const L3: string = `\n` + tab(3);
-const L4: string = `\n` + tab(4);
-
 const renderComponent: (model: {
     component: string;
     widgetImport: IImport;
@@ -211,7 +206,7 @@ function renderProps(props: IProp[]): string {
 const renderPropsTemplate: (props: IProp[]) => string = createTempate(
 `<#~ it :prop #>` +
 
-    tab(2) +
+    TAB2 +
 
     `<#? prop.types && prop.types.length > 0 && !prop.acceptableValues #>` +
         `<#= prop.name #>: ` +
@@ -279,9 +274,5 @@ const renderPropsTemplate: (props: IProp[]) => string = createTempate(
 `<#~#>` + `\b\b`
 );
 
-function tab(i: number): string {
-    return Array(i * 2 + 1).join(" ");
-}
-
 export default generate;
 export { IComponent, INestedComponent, IProp, renderProps };
diff --git a/tools/src/index-generator.ts b/tools/src/index-generator.ts
index d730a1c..c6f97ad 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";
 
 interface IReExport {
   name: string;
diff --git a/tools/src/template.ts b/tools/src/template.ts
index 61c0824..5c82369 100644
--- a/tools/src/template.ts
+++ b/tools/src/template.ts
@@ -1,6 +1,6 @@
 import { template, templateSettings } from "dot";
 
-const settings = {
+const defaultSettings = {
     ...templateSettings,
     conditional: /\<#\?(\?)?\s*([\s\S]*?)\s*#\>/g,
     define: /\<###\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)##\>/g,
@@ -14,11 +14,35 @@ const settings = {
 };
 
 const createTempate = (templateStr: string): ((model: any) => string) => {
-    const templateFunc = template(templateStr, settings);
+    const templateFunc = template(templateStr, defaultSettings);
 
     return (model: any) => (templateFunc(model) as string)
         .replace(/[\s\S]{1}\x08{1}|[\s\S]{2}\x08{2}|[\s\S]{3}\x08{3}/g, "")
         .replace(/\x08/, "");
 };
 
-export default createTempate;
+const L1: string = `\n` + tab(1);
+const L2: string = `\n` + tab(2);
+const L3: string = `\n` + tab(3);
+const L4: string = `\n` + tab(4);
+
+const TAB1: string = tab(1);
+const TAB2: string = tab(2);
+const TAB3: string = tab(3);
+const TAB4: string = tab(4);
+
+function tab(i: number): string {
+    return Array(i * 2 + 1).join(" ");
+}
+
+export {
+    createTempate,
+    L1,
+    L2,
+    L3,
+    L4,
+    TAB1,
+    TAB2,
+    TAB3,
+    TAB4
+};


Test patch:
diff --git a/src/core/component.test.ts b/src/core/component.test.ts
index 0c7072f..d16269c 100644
--- a/src/core/component.test.ts
+++ b/src/core/component.test.ts
@@ -76,7 +76,12 @@ describe("options", () => {
                 sampleProp: "default"
             }
         }).$mount();
-        expect(WidgetClass).toHaveBeenCalledWith(vm.$el, {
+
+        expect(WidgetClass.mock.calls[0][0]).toBe(vm.$el);
+
+        const options = WidgetClass.mock.calls[0][1];
+        delete options.integrationOptions;
+        expect(options).toEqual({
             sampleProp: "default"
         });
     });
@@ -100,6 +105,119 @@ describe("options", () => {
         vm.$props.sampleProp = "new";
         Vue.nextTick(() => {
             expect(Widget.option).toHaveBeenCalledTimes(1);
+            expect(Widget.option).toHaveBeenCalledWith("sampleProp", "new");
+            done();
+        });
+    });
+
+    it("watch array prop changing", (done) => {
+        const arrayValue = [{ text: "text" }];
+        new TestComponent({
+            props: ["sampleProp"],
+            propsData: {
+                sampleProp: arrayValue
+            }
+        }).$mount();
+        const valueChangedCallback = jest.fn();
+        WidgetClass.mock.calls[0][1].integrationOptions.watchMethod(() => {
+            return arrayValue[0].text;
+        }, valueChangedCallback);
+
+        expect(valueChangedCallback).toHaveBeenCalledTimes(1);
+        expect(valueChangedCallback.mock.calls[0][0]).toBe("text");
+
+        arrayValue[0].text = "changedText";
+        Vue.nextTick(() => {
+            expect(valueChangedCallback).toHaveBeenCalledTimes(2);
+            expect(valueChangedCallback.mock.calls[1][0]).toBe("changedText");
+            done();
+        });
+    });
+
+    it("watch array prop changing with Date", (done) => {
+        const date = new Date(2018, 11, 11);
+        const arrayValue = [{ date }];
+        new TestComponent({
+            props: ["sampleProp"],
+            propsData: {
+                sampleProp: arrayValue
+            }
+        }).$mount();
+        const valueChangedCallback = jest.fn();
+        WidgetClass.mock.calls[0][1].integrationOptions.watchMethod(() => {
+            return arrayValue[0].date;
+        }, valueChangedCallback);
+
+        expect(valueChangedCallback).toHaveBeenCalledTimes(1);
+        expect(valueChangedCallback.mock.calls[0][0]).toBe(date);
+
+        arrayValue[0].date = new Date(2018, 11, 11);
+        Vue.nextTick(() => {
+            expect(valueChangedCallback).toHaveBeenCalledTimes(1);
+            expect(valueChangedCallback.mock.calls[0][0]).toBe(date);
+
+            arrayValue[0].date = new Date(2018, 11, 12);
+            Vue.nextTick(() => {
+                expect(valueChangedCallback).toHaveBeenCalledTimes(2);
+                expect(valueChangedCallback.mock.calls[1][0]).toEqual(new Date(2018, 11, 12));
+                done();
+            });
+        });
+    });
+
+    it("watch array prop changing (deep)", (done) => {
+        const arrayValue = [{
+            data: {
+                text: "text"
+            }
+        }];
+        new TestComponent({
+            props: ["sampleProp"],
+            propsData: {
+                sampleProp: arrayValue
+            }
+        }).$mount();
+        const valueChangedCallback = jest.fn();
+        WidgetClass.mock.calls[0][1].integrationOptions.watchMethod(() => {
+            return arrayValue[0].data;
+        }, valueChangedCallback, {
+            deep: true
+        });
+        expect(valueChangedCallback).toHaveBeenCalledTimes(1);
+        expect(valueChangedCallback.mock.calls[0][0]).toEqual({ text: "text" });
+
+        arrayValue[0].data.text = "changedText";
+
+        Vue.nextTick(() => {
+            expect(valueChangedCallback).toHaveBeenCalledTimes(2);
+            expect(valueChangedCallback.mock.calls[0][0]).toEqual({ text: "changedText" });
+            done();
+        });
+    });
+
+    it("watch array prop changing (skipImmediate)", (done) => {
+        const arrayValue = [{
+            text: "text"
+        }];
+        new TestComponent({
+            props: ["sampleProp"],
+            propsData: {
+                sampleProp: arrayValue
+            }
+        }).$mount();
+        const valueChangedCallback = jest.fn();
+        WidgetClass.mock.calls[0][1].integrationOptions.watchMethod(() => {
+            return arrayValue[0].text;
+        }, valueChangedCallback, {
+            skipImmediate: true
+        });
+        expect(valueChangedCallback).toHaveBeenCalledTimes(0);
+
+        arrayValue[0].text = "changedText";
+
+        Vue.nextTick(() => {
+            expect(valueChangedCallback).toHaveBeenCalledTimes(1);
+            expect(valueChangedCallback.mock.calls[0][0]).toEqual("changedText");
             done();
         });
     });
@@ -396,7 +514,11 @@ describe("nested options", () => {
             }
         }).$mount();
 
-        expect(WidgetClass).toHaveBeenCalledWith(vm.$children[0].$el, {
+        expect(WidgetClass.mock.calls[0][0]).toBe(vm.$children[0].$el);
+
+        const options = WidgetClass.mock.calls[0][1];
+        delete options.integrationOptions;
+        expect(options).toEqual({
             nestedOption: {
                 prop1: 123
             }
@@ -419,7 +541,11 @@ describe("nested options", () => {
             }
         }).$mount();
 
-        expect(WidgetClass).toHaveBeenCalledWith(vm.$children[0].$el, {
+        expect(WidgetClass.mock.calls[0][0]).toBe(vm.$children[0].$el);
+
+        const options = WidgetClass.mock.calls[0][1];
+        delete options.integrationOptions;
+        expect(options).toEqual({
             nestedOption: [{
                 prop1: 123
             }]
@@ -444,7 +570,11 @@ describe("nested options", () => {
             }
         }).$mount();
 
-        expect(WidgetClass).toHaveBeenCalledWith(vm.$children[0].$el, {
+        expect(WidgetClass.mock.calls[0][0]).toBe(vm.$children[0].$el);
+
+        const options = WidgetClass.mock.calls[0][1];
+        delete options.integrationOptions;
+        expect(options).toEqual({
             nestedOption: {
                 prop1: 123,
                 subNestedOption: {
@@ -473,7 +603,11 @@ describe("nested options", () => {
             }
         }).$mount();
 
-        expect(WidgetClass).toHaveBeenCalledWith(vm.$children[0].$el, {
+        expect(WidgetClass.mock.calls[0][0]).toBe(vm.$children[0].$el);
+
+        const options = WidgetClass.mock.calls[0][1];
+        delete options.integrationOptions;
+        expect(options).toEqual({
             nestedOption: {
                 prop1: 123,
                 subNestedOption: [{
diff --git a/src/core/helpers.test.ts b/src/core/helpers.test.ts
new file mode 100644
index 0000000..c04333c
--- /dev/null
+++ b/src/core/helpers.test.ts
@@ -0,0 +1,18 @@
+import { toComparable } from "./helpers";
+
+describe("toComparable", () => {
+
+    it("Primitive", () => {
+        expect(toComparable(1)).toBe(1);
+    });
+
+    it("Object", () => {
+        const testObject = { text: 1 };
+        expect(toComparable(testObject)).toBe(testObject);
+    });
+
+    it("Date", () => {
+        const testDate = new Date(2018, 9, 9);
+        expect(toComparable(testDate)).toBe(testDate.getTime());
+    });
+});