Patrick
Patrick

Reputation: 727

Why does vue.js not update the dom with datepicker using moment.js

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>

Upvotes: 7

Views: 7099

Answers (3)

JJPandari
JJPandari

Reputation: 3522

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.

Upvotes: 0

Saurabh
Saurabh

Reputation: 73619

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.

Upvotes: 11

Mihai Vilcu
Mihai Vilcu

Reputation: 1967

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>

Upvotes: 0

Related Questions