Computed object not updated using sync modifier - vue.js

I have a computed property that simply formats the date:
computed: {
items() {
return this.licenseItems.map(function(license) {
license.expires_at = moment(license.expires_at).format('MM/DD/YYYY');
return license;
});
}
}
I pass licenseItems to a form component with the .sync modifier and emit update:field event from the form. In vue dev tools, I can see that licenseItems (data) is properly updated, but items (computed) is still showing the old data so no re-computation was performed.
I have noticed that if I remove the map and just return the licenseItems object from the computed property, it is updated. Is there some issue with Vue's computed property on mapped objects when using the sync modifier?

You should be aware that you're modifying underlying objects in your computed. From your fiddle:
computed: {
mapped: function() {
return this.items.map(function(item) {
let original = item.date;
item.date = moment(item.date).format('MM/DD/YYYY');
console.log(original + ' => ' + item.date);
return item;
});
}
}
Your incrementDates function also modifies the underlying objects directly.
Because each element of items is an object, item is a reference to that same object, so your routine updates the members of items itself. If you intend to modify the object values, you should use a watch instead of a computed. If you want to have a proper computed that does not modify data items, you need to deep copy the objects.
In my example below, you can see that the data value and the computed value are distinct, but the computed is based on the data. Also, the update event works with sync to update the value in the parent, rather than the value being updated directly in the component. You can enter any format of date that Date understands to set the value.
new Vue({
el: '#app',
data: {
licenseItems: [{
expires_at: Date.now()
}]
},
computed: {
items() {
return this.licenseItems.map(function(license) {
const newItem = Vue.util.extend({}, license);
newItem.expires_at = moment(license.expires_at).format('MM/DD/YYYY');
return newItem;
});
}
},
components: {
myUpdater: {
props: ['items'],
methods: {
doUpdate(event, index) {
const newObj = this.items.map(item => Vue.util.extend({}, item));
newObj[index].expires_at = new Date(event.target.value);
console.log("new object", newObj);
this.$emit('update:items', newObj);
}
}
}
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/moment.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="app">
<my-updater :items.sync="licenseItems" inline-template>
<div>
<input v-for="item, index in items" :value="item.expires_at" #change="doUpdate($event, index)">
</div>
</my-updater>
<div v-for="item in items">
{{item.expires_at}}
</div>
</div>

Related

When is a computed property not reactive?

I have a Vue page which gathers information from external JSON sources, and then uses it to compute a property.
Problem: this property is not reactive (= it is not recomputed when underlying data changes)
<div id="app">
<div v-for="tag in filteredTags">{{tag}}</div>
</div>
<script>
new Vue({
el: "#app",
data: {
tags: {},
allTags: {},
},
computed: {
// provides an Array of tags which are selected
filteredTags() {
let t = Object.keys(this.allTags).filter(x => this.allTags[x])
console.log(t)
return t
}
},
mounted() {
// get source tags
fetch("tags.json")
.then(r => r.json())
.then(r => {
this.tags = r
console.log(JSON.stringify(this.tags))
// bootstrap a tags reference where all the tags are selected
Object.keys(this.tags).forEach(t => {
this.allTags[t] = true
});
console.log(JSON.stringify(this.allTags))
})
}
})
</script>
The file which is fetched ({"tag1":["/posts/premier/"],"post":["/posts/premier/","/posts/second/"]}) is correctly processed in mounted(), the output on the console is
{"tag1":["/posts/premier/"],"post":["/posts/premier/","/posts/second/"]}
{"tag1":true,"post":true}
filteredTags is however empty. On the console, I see it displayed (as []) right at the start of the processing of the page, which is initially fine (first compute, when allTags is empty), but is then not computed anymore when allTags changes (after tags.json is fetched, processed and allTags correctly updated).
Why isn't this reactive?
Vue isn't reactive to properties that didn't exist when the object was added to data
Since your tag and allTags are empty objects with no properties (yet), any properties added after aren't reactive automatically.
To solve this you have to use the Vue.Set or this.$set functions provided by Vue.
the Set function accepts the values this.$set(object, key, value)
new Vue({
el: "#app",
data: {
tags: {},
allTags: {},
},
computed: {
// provides an Array of tags which are selected
filteredTags() {
let t = Object.keys(this.allTags).filter(x => this.allTags[x])
return t
}
},
mounted() {
const r = '{"tag1":["/posts/premier/"],"post":["/posts/premier/","/posts/second/"]}';
const rJson = JSON.parse(r);
// You shouldn't need to use $set here as you replace the entire object, instead of adding properties
this.tags = rJson;
Object.keys(this.tags).forEach(t => {
// Change this
//this.allTags[t] = true;
// To this
this.$set(this.allTags, t, true);
});
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<div id="app">
<div v-for="tag in filteredTags">{{tag}}</div>
</div>

watch for the whole array as well as one of the properties of it

Let's say I have a prop such as
[{id:1, name:"first"}, {id:2, name:"second"}]
I have a watcher for this called Points. as soon as parent component changes this array, watcher function gets called in child.
Now, I also want to watch if name field in any of this array's object got changed. Workaround I know is to set deep as true in watcher, but this means this is gonna make the watcher for each property of the object. As I have a very huge array of huge objects, I don't want to make this many watcher.
So is there a way to make watcher for the whole array and also one of the properties of the array's object?
You can mark the non-reactive properties as non-configurable, and Vue will not detect changes in their values.
let arr = [{id:1, name:"first"}, {id:2, name:"second"}];
arr.forEach(item => Object.defineProperty(item, "id", {configurable: false}));
Alternatively, you could use a shallow watcher and require code that modifies the reactive properties to use Vue.set() instead of simple assignment.
Vue.set(arr[0], "name", "new name"); // don't use arr[0].name = "new name";
You can create a child component, where you bind objects to the array and place the watcher inside the child component as below:
Parent.vue
<template>
<child-component v-for="point in points" :point="point" ></child-component>
</template>
data: {
return {
points: [{id:1, name:"first"}, {id:2, name:"second"}]
}
}
Child.vue:
props: ['point']
...
watch: {
name: function(newVal){
// watch name field
}
}
One way would be to create a computed property that just touches the bits you care about and then watch that instead.
new Vue({
el: '#app',
data () {
return {
points: [{id: 1, name: 'first'}, {id: 2, name: 'second'}]
}
},
computed: {
pointStuffICareAbout () {
return this.points.map(point => point.name)
}
},
methods: {
updateId () {
this.points[0].id = Math.round(Math.random() * 1000)
},
updateName () {
this.points[0].name = Math.random().toString(36).slice(2, 7)
},
addItem () {
this.points.push({id: 3, name: 'third'})
}
},
watch: {
pointStuffICareAbout () {
console.log('watcher triggered')
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<button #click="updateId">Update id</button>
<button #click="updateName">Update name</button>
<button #click="addItem">Add item</button>
<p>
{{ points }}
</p>
</div>

How to clone props object and make it non-reactive [duplicate]

This question already has answers here:
What is the most efficient way to deep clone an object in JavaScript?
(67 answers)
Closed 12 days ago.
I have some form data which I share with children components through props. Now I want to clone the prop object and make it non-reactive. In my case I want the user to be able to modify the props value without actually changing the cloned value. The cloned value should only be there to show the user what the form was when editing. Below code shows this:
<template>
<div>
<div v-if="computedFormData">
original prop title: {{orgData.title}}
new title:
<input type="text" v-model="formData.title"/>
//changing data here will also change orgData.title
</div>
</div>
</template>
<script>
export default {
props: ['formData'],
data() {
return {
orgData: [],
}
},
computed: {
computedFormData: function () {
this.orgData = this.formData;
return this.orgData;
},
},
methods: {
},
}
</script>
I have tried with Object.freeze(testData); but it doesnt work, both testData and orgData are reactive. Note also that using mounted or created property does not render orgData so I'm forced to use the computed property.
Try copying the prop values with Object.assign. No more issue with reactivity since the new, assigned values are just the copy instead of the reference to the source.
If your data object is a lot more complex, I'd recommend deepmerge in place of Object.assign.
Vue.component('FormData', {
template: `
<div>
<div v-if="testData">
<p>Original prop title: <strong>{{orgData.title}}</strong></p>
<p>Cloned prop title:</p>
<input type="text" v-model="testData.title" />
</div>
</div>
`,
props: ['orgData'],
data() {
return {
testData: Object.assign({}, this.orgData)
}
}
});
const vm = new Vue({
el: '#app',
data() {
return {
dummyForm: {
title: 'Some title'
}
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form-data :org-data="dummyForm"></form-data>
</div>
Not entirely sure why but using Object.assign on a computed property did not work for me. I solved it by using a watch property for the props value:
watch:{
formData(){
this.orgData = Object.assign({}, this.formData)
}
},
Object.assign is merely a shallow copy. If you have a copy consists that of only primitive data types (string, number, bigint, boolean, undefined, symbol, and null) it's ok. It to remove its reactivity. But, if you have a copy that has reference types you can’t shallow clone it to remove its reactivity.
For depping clone you can use the JSON.parse(JSON.stringify()) pattern. But keep in mind that is going to work if your data consists of supported JSON data types.
props: ['orgData'],
data() {
return {
cloneOrgData: JSON.parse(JSON.stringify(this.orgData))
}
}

Computed property doesn't update upon changing data variable

I have a data variable named monRaw that is a Moment object representing the first day of current week:
...
data() {
return {
monRaw: moment().startOf('isoWeek')
}
},
...
Then I have a computed property named weekTitle that displays it along with some text (it does more than that in my project, but I simplified it here):
...
computed: {
weekTitle: function() {
alert('updating computed property...');
return `Current week starting on ${this.monRaw.format('DD')}`;
}
},
...
Finally, a method named viewNextWeek() changes my data variable to the following week's first day:
...
methods: {
viewNextWeek: function() {
this.monRaw = this.monRaw.add(1, 'weeks');
},
...
In my template, I display my computed property along with a button triggering my method:
{{ weekTitle }}
<button #click="viewNextWeek">Next Week</button>
For some reason though, my computed property does NOT get updated when I change the data variable it's based on.
It doesn't even try to: my alert() gets triggered on page load only, not any more after that when clicking the button.
What's wrong here?
Moment.add mutates the moment variable, so you are just assigning the same object back to itself, which results in no change.
Per the docs:
If you want to create a copy and manipulate it, you should use
moment#clone before manipulating the moment.
new Vue({
el: '#app',
data: {
monRaw: moment().startOf('isoWeek')
},
computed: {
weekTitle: function() {
alert('updating computed property...');
return `Current week starting on ${this.monRaw.format('DD')}`;
}
},
methods: {
viewNextWeek: function() {
const newValue = this.monRaw.clone().add(1, 'weeks');
this.monRaw = newValue;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
<script src="https://unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
{{ weekTitle }}
<button #click="viewNextWeek">Next Week</button>
</div>

Data Fetch in Vue.js Single File Component

my data fetch works fine when is used globally but once I stick in the single file component is not returning the items. What I'm doing wrong?
ladeditems.vue
<template>
<div>
<ul v-for="item in items">
<li>
{{item.title}}
</li>
</ul>
</div>
</template>
<script>
export default {
components: {'tiny-slider': VueTinySlider},
name: 'ladeditems',
data: {
items: null
},
methods: {
fetchData: function () {
let self = this
const myRequest = new Request('https://jsonplaceholder.typicode.com/posts')
fetch(myRequest)
.then((response) => { return response.json() })
.then((data) => {
self.items = data
// console.log(self.items)
}).catch( error => { console.log(error); });
}
},
mounted() {
this.fetchData()
}
}
</script>
Your data declaration is incorrect, it should be like this:
data: function () {
return {
items: null
}
}
This info is here: data. In short it has to be a function that returns an object. This should allow the property to be reactive to your changes.
Also worth noting that fetch isn't declared in the code you've provided so I assume it's a global declaration. If it isn't and it's a mixin then you'll need to scope it with this.fetch(...)
https://v2.vuejs.org/v2/api/#data
When defining a component, data must be declared as a function that returns the initial data object, because there will be many instances created using the same definition. If we use a plain object for data, that same object will be shared by reference across all instances created! By providing a data function, every time a new instance is created we can call it to return a fresh copy of the initial data.