Vue: Do watchers on deep nested objects log oldVal and newVal? - vue.js

I have a watcher on a deep nested object. I am using Vue.set() to add a reactive property to the object. This is triggering the watcher but the both the newVal and oldVal console logs are showing the data with the new property added to it already rather than the oldVal showing what it was prior to adding the new property to it.
<button #click="addData">Add</button>
data() {
return {
myData: {
time: {
}
}
}
},
watch: {
myData: {
handler(newVal, oldVal) {
console.log("NEW", newVal);
console.log("OLD", oldVal);
},
deep: true
}
},
methods: {
addData() {
this.$set(this.myData.time, 'other','testing')
}
}

As mentioned by #Terry, vue doesnt keep a referency to the oldValue, see what the docs says:
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
There is some ways you can solve that:
Using vanilla javascript
Add a computed property that returns your data property converting to a JSON string.
Use JSON.parse on your Watch function to convert the string back to a object.
data() {
return {
myData: {
time: {
}
}
}
},
computed: {
computedMyData() {
return JSON.stringify(this.myData);
}
},
watch: {
computedMyData: {
handler(newValJSON, oldValJSON) {
let newVal = JSON.parse(newValJSON),
oldVal = JSON.parse(oldValJSON);
console.log("NEW", newVal);
console.log("OLD", oldVal);
},
deep: true
}
},
methods: {
addData() {
this.$set(this.myData.time, 'other','testing')
}
}
Fiddle: https://jsfiddle.net/mLbuf6t0/
Using LODASH
Add a computed property that returns a deep clone from your data property with cloneDeep function.
data() {
return {
myData: {
time: {
}
}
}
},
computed: {
computedMyData() {
return _.cloneDeep(this.myData)
}
},
watch: {
computedMyData: {
handler(newVal, oldVal) {
console.log("NEW", newVal);
console.log("OLD", oldVal);
},
deep: true
}
},
methods: {
addData() {
this.$set(this.myData.time, 'other','testing')
}
}
Fiddle: https://jsfiddle.net/mLbuf6t0/2/

Related

How do I make Vue 2 Provide / Inject API reactive?

I set up my code as follows, and I was able to update checkout_info in App.vue from the setter in SomeComponent.vue, but the getter in SomeComponent.vue is not reactive.
// App.vue
export default {
provide() {
return {
checkout_info: this.checkout_info,
updateCheckoutInfo: this.updateCheckoutInfo
}
},
data() {
return {
checkout_info: {},
}
},
methods: {
updateCheckoutInfo(key, value) {
this.checkout_info[key] = value
}
}
}
// SomeComponent.vue
export default {
inject: ['checkout_info', 'updateCheckoutInfo']
computed: {
deliveryAddress: {
get() { return this.checkout_info.delivery_address }, // <---- Not reactive??
set(value) { return this.updateCheckoutInfo('delivery_address', value) }
}
}
}
I found the answer after many hours of searching. You have to use Object.defineProperty to make it reactive. I'm not sure if this is the best approach, but this is a working example.
export default {
data() {
return {
checkout_info: {},
}
},
provide() {
const appData = {}
Object.defineProperty(appData, "checkout_info", {
enumerable: true,
get: () => this.checkout_info,
})
return {
updateCheckoutInfo: this.updateCheckoutInfo,
appData,
}
}
}
You can later access it via this.appData.checkout_info
This note from official documentation.
Note: the provide and inject bindings are NOT reactive. This is
intentional. However, if you pass down an observed object, properties
on that object do remain reactive.
I think this is the answer to your question.
source:
https://v2.vuejs.org/v2/api/#provide-inject
I would put the values in an object:
var Provider = {
provide() {
return {
my_data: this.my_data
};
},
data(){
const my_data = {
foo: '1',
fuu: '2'
};
return {
my_data;
}
}
}
var Child = {
inject: ['my_data'],
data(){
console.log(my_data.foo);
return {};
},
}
When are object properties they are reactive. I don't know if this is the correct solution but it works in my case.

Vue computed methods can't change data using setter in template

I need change data using computed:
<template>
<div>{{ userDataTest }}</div>
</template>
props: {
exampleData: {
type: Object,
required: true,
},
},
computed: {
userDataTest: {
get: function() {
return this.exampleData;
},
set: function(newValue) {
console.log(newValue);
return newValue;
},
},
}
mounted () {
setTimeout(() => {
console.log('Change now to null!');
this.userDataTest = null;
}, 5000);
},
I get data using props, next I create computed methods with getter and setter. I added userDataTest in <template>. And the I change (using mounted) data in this.userDataTest to null using setter.
In console.log(newValue); in setter I see newValue is null, but in <template> nothing change still I have data from getter.
Why setter not change data in <template> to null ?
It seems you're trying to set the computed property's value by returning a new value, but Vue doesn't actually check the setter's return value. Perhaps you were trying to proxy a data variable through a computed property. If so, the setter should set that data variable in the setter body.
For instance, your component could declare a data variable, named userData, which always has the latest value of the exampleData prop through a watcher:
export default {
props: {
exampleData: Object
},
data() {
return {
userData: {}
}
},
watch: {
exampleData(exampleData) {
this.userData = exampleData
}
},
}
Then, your template and computed prop would use userData instead:
<template>
<div>{{ userData }}</div>
</template>
<script>
export default {
//...
computed: {
userDataTest: {
get() {
return this.userData
},
set(newValue) {
this.userData = newValue
}
}
}
}
</script>
Mutating a prop locally is considered an anti-pattern
However, you can use the .sync modifier as shown below, but you can't set the prop to null because you are specifying that it has to be an Object type.
Vue.component('my-component', {
template: `<div>{{ userDataTest }}</div>`,
props: {
exampleData: {
type: Object,
required: true
}
},
computed: {
userDataTest: {
get: function() {
return this.exampleData
},
set: function(newValue) {
this.$emit('update:exampleData', newValue)
}
}
},
mounted() {
setTimeout(() => {
console.log('Change now!')
this.userDataTest = {}
}, 2500)
}
})
new Vue({
el: '#app',
data() {
return {
exampleData: {
foo: 'bar'
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<my-component :example-data.sync="exampleData"></my-component>
</div>

Change data between few router-view

I have two components, and can't pass data from one to another component
at first router-view have
data() {
return {
mode: true,
}
},
<input type="checkbox" class="switch-mode" v-model="mode" #change="$root.$emit('switch-mode', mode)">
and other is
data() {
return {
filter: {
mode: false,
order: 'DESC'
},
}
},
mounted() {
this.$root.$on('switch-mode', function (EventGrid) {
console.log('Grid mode is '+EventGrid); //this works it return true,false
this.filter.mode = EventGrid; // not working this.filter is undefined"
})
},
This happens because your function doesn't know what this is, you should explicitly tell it to use your component:
mounted() {
this.$root.$on(
'switch-mode',
(function (EventGrid) { ... }).bind(this)
)
}
Or, more effectively and modern, use an arrow function:
mounted() {
this.$root.$on('switch-mode', (EventGrid) => { ... })
}

How to commit received data to Vue store?

I'm trying to:
get element's data #click using getDetails method and put it into fileProperties: []
and then send that data to store using fileDetails computed property
This worked for my other components which have v-model and simple true/false state, but I'm not sure how to send the created by the method array of data to the store properly.
In other words, how do I make this computed property to get the data from fileProperties: [] and commit it to store? The fileDetails computed property below is not committing anything.
Code:
[...]
<div #click="getDetails(file)"></div>
[...]
<script>
export default {
name: 'files',
data () {
return {
fileProperties: []
}
},
props: {
file: Object
},
methods: {
getDetails (value) {
this.fileProperties = [{"extension": path.extname(value.path)},
{"size": this.$options.filters.prettySize(value.stat.size)}]
}
},
computed: {
isFile () {
return this.file.stat.isFile()
},
fileDetails: {
get () {
return this.$store.state.Settings.fileDetails
},
set (value) {
this.$store.commit('loadFileDetails', this.fileProperties)
}
}
}
}
</script>
store module:
const state = {
fileDetails: []
}
const mutations = {
loadFileDetails (state, fileDetails) {
state.fileDetails = fileDetails
}
}
Example on codepen:
https://codepen.io/anon/pen/qxjdNo?editors=1011
In this codepen example, how can I send over the dummy data [ { "1": 1 }, { "2": 2 } ] to the store on button click?
You are never setting the value for fileDetails, so the set method of the computed property is never getting called. Here's the documentation on computed setters.
If the fileProperties data is really just the same as the fileDetails data, then get rid of it and set fileDetails directly in your getDetails method.
Here's a working example:
const store = new Vuex.Store({
state: {
fileDetails: null
},
mutations: {
loadFileDetails (state, fileDetails) {
state.fileDetails = fileDetails
}
}
})
new Vue({
el: '#app',
store,
data() {
return {
fileProperties: null
}
},
methods: {
getDetails (value) {
this.fileDetails = [{"1": 1}, {"2": 2}]
}
},
computed: {
fileDetails: {
get () {
return this.$store.state.fileDetails
},
set (value) {
this.$store.commit('loadFileDetails', value)
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<div id="app">
<h1>element data:</h1>
{{fileDetails}}
<hr>
<h1>store data:</h1>
<p>(should display the same data on button click)</p>
{{fileDetails}}
<hr>
<button #click="getDetails">btn</button>
</div>

`This` is undefined in vue.js watcher

I have component with watcher
props: {
propShow: { required: true, type: Boolean }
},
data() {
return {
show: this.propShow
}
},
watch: {
propShow: {
handler: (val, oldVal) => {
this.show = val;
}
}
}
Whenever parent component changes propShow this component must update it's show property. This component also modifies show property, that's why I need both: show and propShow, because Vue.js does not allow to change properties directly.
This line
this.show = val;
causes error
TypeError: Cannot set property 'show' of undefined
because this inside handler is undefined.
Why?
You will have to use function syntax here, as warned in the docs here:
Note that you should not use an arrow function to define a watcher (e.g. searchQuery: newValue => this.updateAutocomplete(newValue)). The reason is arrow functions bind the parent context, so this will not be the Vue instance as you expect and this.updateAutocomplete will be undefined.
So your code should be:
watch: {
propShow: {
handler: function(val, oldVal) {
this.show = val;
}
}
}
"function" is not es6 code, you should better write:
watch: {
propShow: {
handler(val, oldVal) {
this.show = val;
}
}
}