Repository: DevExpress/devextreme-vue
Pull request #159 (2018-10-08 11:51:06)

Add watch method
Add watch method to check modifications in arrays


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/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;
+}


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());
+    });
+});