Reputation: 727
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'><</button>{{formattedSelectedDate}}<button v-on:click='increment'>></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
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
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.
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
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'><</button>
{{formattedSelectedDate}}
<button v-on:click='increment'>></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