7

I am writing a (very) simple datepicker control in vue.js, using moment.js for formatting and mutating the date.

The problem I'm having is that even though the date instance in the component is modified when I click either button, the display does not update.

I've tried changing this to a simple integer instead of a date instance, and that works as expected (the DOM is updated properly)

Here is the source code that I have for this:

App.js

Vue.component("datePicker", {
    props: ["value"],
    data: function() {
        return { selectedDate: moment(this.value) };
    },
    template: "<div><button v-on:click='decrement'>&lt;</button>{{formattedSelectedDate}}<button v-on:click='increment'>&gt;</button></div>",
    methods: {
        increment: function () {
            this.selectedDate.add(1, "days");
            this.$emit('input', this.selectedDate);
        },
        decrement: function () {
            this.selectedDate.subtract(1, "days");
            this.$emit('input', this.selectedDate);
        }
    },
    computed: {
        formattedSelectedDate: function() {
            return this.selectedDate.format("YYYY-MM-DD");
        }
    }
});

var PointTracker = new Vue({
    el: "#PointTracker",
    data: {
        selectedDate: moment(),
        todoItems: {}
    }
});

Index.html

<html>
    <head>
        <meta charset="utf-8">
        <title>Point Tracker</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>

        <div id="PointTracker">
            <date-picker v-model="selectedDate">
        </div>

        <script src="node_modules/moment/moment.js"></script>
        <script src="node_modules/vue/dist/vue.js"></script>
        <script src="node_modules/vue-resource/dist/vue-resource.js"></script>
        <script src="app.js"></script>
    </body>
</html>
Saurabh
  • 71,488
  • 40
  • 181
  • 244
Patrick
  • 697
  • 2
  • 9
  • 19
  • 1
    I have create a [fiddle](http://jsfiddle.net/mimani/pLcfyrvy/) of your code, also I have added few more element in div, not sure why they are not rendering. – Saurabh Dec 04 '16 at 15:33

3 Answers3

11

You have to change the reference of selectedDate, as it is returned from moment functions, they are always of same reference, so vue watchers are not triggered for these.

You have to make following changes to change the reference:

methods: {
    increment: function () {
        this.selectedDate = moment(this.selectedDate).add(1, "days")
    },
    decrement: function () {
        this.selectedDate = moment(this.selectedDate).subtract(1, "days")
    }
},

Working fiddle: http://jsfiddle.net/mimani/pLcfyrvy/1/


Following is implementation of add/subtract from moment library:

function addSubtract (duration, input, value, direction) {
    var other = createDuration(input, value);

    duration._milliseconds += direction * other._milliseconds;
    duration._days         += direction * other._days;
    duration._months       += direction * other._months;

    return duration._bubble();
}

// supports only 2.0-style add(1, 's') or add(duration)
export function add (input, value) {
    return addSubtract(this, input, value, 1);
}

// supports only 2.0-style subtract(1, 's') or subtract(duration)
export function subtract (input, value) {
    return addSubtract(this, input, value, -1);
}

It returns the same object so the reference is same for the date object.


Why It happened

It happens because moment.js is a isn't immutable, what it means is when you call add/subtract function on a moment object, it returns the same object, with after changing the properties.

There are some caveats on reactivity of vue and as Object.observe is obsolete now, it can not track if an javascript object has changed internally, unless you clone the object and create a new object which is needed in your case.

There are other workarounds also for this including using frozen-moment library.

tony19
  • 125,647
  • 18
  • 229
  • 307
Saurabh
  • 71,488
  • 40
  • 181
  • 244
  • This approach works, but I have an additional clarification question: why does vue require the reference to change? Shouldn't it be enough that the underlying properties change? Or are they not monitored – Patrick Dec 05 '16 at 01:10
  • @Patrick It happened because moment library is mutable, I have added explanation in the answer. – Saurabh Dec 05 '16 at 04:01
  • I've accepted this answer because of the extra information. One final question to make sure I understand this right, if the prop was something like { field: "abc" } and I changed this.field to "cde" it wouldn't update the dom either? – Patrick Dec 05 '16 at 23:54
  • @Patrick What you want is [Dynamic-Props](https://vuejs.org/v2/guide/components.html#Dynamic-Props), syntax is : v-bind:my-field="field", I hope this is what you are asking. As par SO guidelines, I will recommend you to ask a separate question, with details and reproduction steps, if issue persists. – Saurabh Dec 06 '16 at 03:40
  • Thanks for this question if was already suspecting this. Somehow I still go an input field to work with reactivity so I was struggling for a long time with this. – Stephan-v Jul 20 '17 at 08:07
0

The issue is that the reference to the selectedDate never changes. It is the same object even if you are calling methods on it.
One approach here would be to store the date in a different format. As a date object, as a number etc. Then when you are making a change on it you can generate a new instance of that data.

Here is an example:

var datePicker = Vue.component("datePicker", {
    data: function() {
        return { selectedDate: moment().toDate() }
    },
    template: `<div>
        <button v-on:click='decrement'>&lt;</button>
          {{formattedSelectedDate}}
        <button v-on:click='increment'>&gt;</button>
        {{selectedDate}}
      </div>`,
    methods: {
        increment: function () {
            var currentDate = moment(this.selectedDate);
            currentDate.add(1, "days");
            this.selectedDate = currentDate.toDate();
        },
        decrement: function () {
            var currentDate = moment(this.selectedDate);
            currentDate.subtract(1, "Days");
            this.selectedDate = currentDate.toDate();
        }
    },
    computed: {
        formattedSelectedDate: function() {
            return moment(this.selectedDate).format("YYYY-MM-DD");
        }
    }
});

var PointTracker = new Vue({
    el: "#PointTracker",
    data: {
        date: 0
    },
    components: {
        datePicker
    }
});
<div id="PointTracker">
  <date-picker />
</div>

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment.js"></script>
Mihai Vilcu
  • 1,947
  • 18
  • 24
0

This is because moment and vue both rely on defining objects' getters and setters, which results in one overriding another in your case. (So changing the reference works because vm.selectedDate.set is overridden while vm.set is not)

From moment's doc:

Moment.js uses overloaded getters and setters.

From vue's doc:

Vue will recursively convert its properties into getter/setters to make it “reactive”. The object must be plain: native objects such as browser API objects and prototype properties are ignored. A rule of thumb is that data should just be data - it is not recommended to observe objects with its own stateful behavior.`

A quick validation: I added console.log(this.selectedDate.get) to the fiddle saurabh created, the logged function is stringGet in moment.js, I then checked vm.foo's get in my project, it's proxyGetter which is a wrapper for reactiveGetter, both in vue.runtime.common.js.

tony19
  • 125,647
  • 18
  • 229
  • 307
JJPandari
  • 3,454
  • 1
  • 17
  • 24