Vue computed property not responding to state change - vue.js

I cannot figure out why the details computed property in the following component is not updating when the fetch() method is called:
<template>
<div>
{{ haveData }} //remains undefined
</div>
</template>
<script>
export default {
props: {
group: {
type: Object,
required: true
},
},
computed: {
currentGroup() {
return this.$store.getters['user/navbar_menu_app_current_group'](
this.group.id
)
/*-- which is the following function
navbar_menu_app_current_group: state => item => {
return state.menu.find(m => {
return m.id == item
})
}
*/
/*-- this function returns an object like so
{
id: 1,
label: 'kity cats',
}
***details --> IS NOT DEFINED. If I add it to the current group as null, my problem goes away. However, this is a previous API call that does not set the `details` parameter.
*/
},
details() {
let c = this.currentGroup.details
console.log(c) // returns undefined, which makes sense, but it should be updated after this.fetch() is called
return c
},
haveData() {
return this.details != null
}
},
methods: {
async fetch() {
await this.$store.dispatch(
'user/navbar_menu_app_details_get',
this.group.id
)
//This is setting the "details" part of the state on menu which is referred to in the computed properties above
//Previous to this there is no state "this.group.details"
//If I add a console log to the mutation the action calls, I get what is expected.
}
},
created() {
if (!this.haveData) {
this.fetch()
}
}
}
</script>
If I change the array items to include details, it works:
{
id: 1,
label: 'kity cats',
details: null // <-- added
}
The unfortunate part is that the array is created from a large API call, and adding the details seems unnecessary, as it may never be needed.
How can I get the computed properties to work without adding the details:null to the default state?
Attempt 1:
// Vuex mutation
navbar_menu_app_details_set(state, vals) {
let app = state.menu.find(item => {
return item.id == vals[0] //-> The group id passing in the dispatch function
})
//option 1 = doesn't work
app = { app, details: vals[1] } //-> vals[1] = the details fetched from the action (dispatch)
//option 2 = doesnt work
app.details = vals[1]
//option 3 = working but want to avoid using Vue.set()
import Vue from 'vue' //Done outside the actual function
Vue.set( app, 'details', vals[1])
},
Attempt 2:
// Vuex action
navbar_menu_app_details_get(context, id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
context.commit('navbar_menu_app_details_set', [
context.getters.navbar_menu_app_current(id), //-> the same as find function in the mutation above
apps[id]
])
resolve()
}, 1000)
})
}
// --> mutation doesn't work
navbar_menu_app_details_set(state, vals) {
vals[0].details = vals[1]
},

The Vue instance is available from a Vuex mutation via this._vm, and you could use vm.$set() (equivalent to Vue.set()) to add details to the menu item:
navbar_menu_app_details_set(state, vals) {
let app = state.menu.find(item => {
return item.id == vals[0]
})
this._vm.$set(app, 'details', vals[1])
},

All Objects in Vue are reactive and are designed in a way such that only when the object is re-assigned, the change will be captured and change detection will happen.
Such that, in your case, following should do fine.
app = { ...app, details: vals[1] }

Related

Setting intial local data from Vuex store giving "do not mutate" error

I thought I understood the correct way to load inital state data from Vuex into the local data of a component, but why is this giving me “[vuex] do not mutate vuex store state outside mutation handlers.” errors! I am using a mutation handler!
I want my component data to start empty, unless coming back from a certain page (then it should pull some values from Vuex).
The component is using v-model=“selected” on a bunch of checkboxes. Then I have the following:
// Template
<grid-leaders
v-if="selected.regions.length"
v-model="selected"
/>
// Script
export default {
data() {
return {
selectedProxy: {
regions: [],
parties: [],
},
}
},
computed: {
selected: {
get() {
return this.selectedProxy
},
set(newVal) {
this.selectedProxy = newVal
// If I remove this next line, it works fine.
this.$store.commit("SET_LEADER_REGIONS", newVal)
},
},
},
mounted() {
// Restore saved selections if coming back from a specific page
if (this.$store.state.referrer.name == "leaders-detail") {
this.selectedProxy = {...this.$store.state.leaderRegions }
}
}
}
// Store mutation
SET_LEADER_REGIONS(state, object) {
state.leaderRegions = object
}
OK I figured it out! The checkbox component (which I didn't write) was doing this:
updateRegion(region) {
const index = this.value.regions.indexOf(region)
if (index == -1) {
this.value.regions.push(region)
} else {
this.value.regions.splice(index, 1)
}
this.$emit("input", this.value)
},
The line this.value.regions.push(region) is the problem. You can't edit the this.value prop directly. I made it this:
updateRegion(region) {
const index = this.value.regions.indexOf(region)
let regions = [...this.value.regions]
if (index == -1) {
regions.push(region)
} else {
regions.splice(index, 1)
}
this.$emit("input", {
...this.value,
regions,
})
},
And then I needed this for my computed selected:
selected: {
get() {
return this.selectedProxy
},
set(newVal) {
// Important to spread here to avoid Vuex mutation errors
this.selectedProxy = { ...newVal }
this.$store.commit("SET_LEADER_REGIONS", { ...newVal })
},
},
And it works great now!
I think the issue is that you can't edit a v-model value directly, and also you also have to be aware of passing references to objects, and so the object spread operator is a real help.

How to get updated value from vuex store in component

I want to show a progress bar in a component. The value of the progress bar should be set by the value of onUploadProgress in the post request (axios). Till so far, that works well. The state is updated with that value correctly.
Now, I am trying to access that value in the component. As the value updates while sending the request, I tried using a watch, but that didn't work.
So, the question is, how to get that updated value in a component?
What I tried:
component.vue
computed: {
uploadProgress: function () {
return this.$store.state.content.object.uploadProgressStatus;
}
}
watch: {
uploadProgress: function(newVal, oldVal) { // watch it
console.log('Value changed: ', newVal, ' | was: ', oldVal)
}
}
content.js
// actions
const actions = {
editContentBlock({ commit }, contentObject) {
commit("editor/setLoading", true, { root: true });
let id = object instanceof FormData ? contentObject.get("id") : contentObject.id;
return Api()
.patch(`/contentblocks/${id}/patch/`, contentObject, {
onUploadProgress: function (progressEvent) {
commit("setOnUploadProgress", parseInt(Math.round((progressEvent.loaded / progressEvent.total) * 100)));
},
})
.then((response) => {
commit("setContentBlock", response.data.contentblock);
return response;
})
.catch((error) => {
return Promise.reject(error);
});
},
};
// mutations
const mutations = {
setOnUploadProgress(state, uploadProgress) {
return (state.object.uploadProgressStatus = uploadProgress);
},
};
Setup:
Vue 2.x
Vuex
Axios
Mutations generally are not meant to have a return value, they are just to purely there set a state value, Only getters are expected to return a value and dispatched actions return either void or a Promise.
When you dispatch an action, a dispatch returns a promise by default and in turn an action is typically used to call an endpoint that in turn on success commits a response value via a mutation and finally use a getter to get the value or map the state directly with mapState.
If you write a getter (not often required) then mapGetters is also handy to make vuex getters available directly as a computed property.
Dispatch > action > commit > mutation > get
Most of your setup appears correct so it should be just a case of resolving some reactivity issue:
// content.js
const state = {
uploadProgress: 0
}
const actions = {
editContentBlock (context, contentObject) {
// other code
.patch(`/contentblocks/${id}/patch/`, contentObject, {
onUploadProgress: function (progressEvent) {
context.commit('SET_UPLOAD_PROGRESS',
parseInt(Math.round((progressEvent.loaded / progressEvent.total) * 100)));
},
}
// other code
}
}
const mutations = {
SET_UPLOAD_PROGRESS(state, uploadProgress) {
state.uploadProgress = uploadProgress
}
}
// component.vue
<template>
<div> {{ uploadProgress }} </div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState('content', ['uploadProgress']) // <-- 3 dots are required here.
}
}
</script>

How to define an empty Vuex state and then check it is not empty/null in Vue component?

I have a Vuex store which defines a state as null:
const state = {
Product: null // I could do Product:[] but that causes a nested array of data
};
const getters = {
getProduct: (state) => state.Product
};
const actions = {
loadProduct({commit}, {ProudctID}) {
axios.get(`/api/${ProductID}`).then(response => {commit('setProduct', response.data)})
.catch(function (error) {
//error handler here
}
}
};
const mutations = {
setProduct(state, ProductData) {
state.Product = ProductData
}
};
In my Vue component I want to display the Product data when it is available. So I did this:
<template>
<div v-if="Product.length">{{Product}}</div>
</template>
When I run it, I get an error stating
Vue warn: Error in render: "TypeError: Cannot read property 'length'
of null"
Okay, so I tried this which then does nothing at all (throws no errors and never displays the data):
<template>
<div v-if="Product != null && Product.length">{{Product}}</div>
</template>
What is the correct way to display the Product object data if it is not null? Product is a JSON object populated with data from a database which is like this:
[{ "ProductID": 123, "ProductTitle": "Xbox One X Gaming" }]
My Vue component gets the data like this:
computed:
{
Product() {
return this.$store.getters.getProduct
}
}
,
serverPrefetch() {
return this.getProduct();
},
mounted() {
if (this.Product != null || !this.Product.length) {
this.getProduct();
}
},
methods: {
getProduct() {
return this.$store.dispatch('loadProduct')
}
}
If I look in Vue Dev Tools, it turns out that Product in my Vue component is always null but in the Vuex tab it is populated with data. Weird?
This is a classic case to use computed:
computed: {
product() {
return this.Product || [];
}
}
In the store function when you do the request you can check
examoe
const actions = {
loadProduct({commit}, {ProudctID}) {
if (this.product.length > 0) {// <-- new code
return this.product // <-- new code
} else { // <-- new code
// do the http request
axios.get(`/api/${ProductID}`)
.then(response => {
commit('setProduct', response.data)}
)
.catch(function (error) {
//error handler here
}
}// <-- new code
}
};

Vuex state change on object does not trigger rerender

I have a variable in the vuex store called permissions. And i want my component to trigger a rerender when the getPermissions changes. In the vue devtools i clearly see that the state has changed in the store, but the component stil get the old state from getPermissions. In order for me to see changes, I have to do a refresh. Has it something to do with the way i mutate it? or the fact that it is an object?
It looks like this when populated:
permissions: {
KS1KD933KD: true,
KD9L22F732: false
}
I use this method to do mutations on it and a getter to get it:
const getters = {
getPermissions: state => state.permissions
};
const mutations = {
set_recording_permissions(state, data) {
let newList = state.permissions;
newList[data.key] = data.bool;
Vue.set(state, 'permissions', newList);
}
};
And in the component i use mapGetters to get access to it
computed: {
...mapGetters('agentInfo',['getPermissions'])
}
In order to update the permissions value i use this action (it does require a succesfull api request before updating the value) :
const actions = {
async setRecordingPermissions({ commit }, data) {
let body = {
agentId: data.userName,
callId: data.callId,
allowUseOfRecording: data.allowUseOfRecording
};
try {
await AgentInfoAPI.editRecordingPermissions(body).then(() => {
commit('set_recording_permissions', { key: data.callId, bool: data.allowUseOfRecording });
commit('set_agent_info_message', {
type: 'success',
text: `Endret opptaksrettigheter`
});
});
} catch (error) {
console.log(error);
commit('set_agent_info_message', {
type: 'error',
text: `Request to ${error.response.data.path} failed with ${error.response.status} ${error.response.data.message}`
});
}
}
}
Since the getter only returns state variable you should use mapState, if you want to access it directly.
computed: mapState(['permissions'])
However, you can also use mapGetters, but then in your template, have to use getPermissions and not permissions.
Example template:
<ul id="permissions">
<li v-for="permission in getPermissions">
{{ permission }}
</li>
</ul>
If you have done this it is probably an issue with the object reference. You use Vue.set, but you set the same object reference. You have to create a new object or set the key you want to update directly.
new object
let newList = { ...state.permissions };
Vue.set
Vue.set(state.permission, data.key, data.value);
I don't know what the rest of you code looks like, but you will need to use actions to correctly mutate you store.
For example:
const actions = {
setName({ commit }, name) {
commit('setName', name);
},
}

Onsen + VueJS: Call back from child component (using onsNavigatorProps)

Per documentation here
If page A pushes page B, it can send a function as a prop or data that modifies page A’s context. This way, whenever we want to send anything to page A from page B, the latter just needs to call the function and pass some arguments:
// Page A
this.$emit('push-page', {
extends: pageB,
onsNavigatorProps: {
passDataBack(data) {
this.dataFromPageB = data;
}
}
});
I am following this idea. Doing something similar with this.$store.commit
I want to push AddItemPage and get the returned value copied to this.items
//Parent.vue
pushAddItemPage() {
this.$store.commit('navigator/push', {
extends: AddItemPage,
data() {
return {
toolbarInfo: {
backLabel: this.$t('Page'),
title: this.$t('Add Item')
}
}
},
onsNavigatorProps: {
passDataBack(data) {
this.items = data.splice() //***this*** is undefined here
}
}
})
},
//AddItemPage.vue
...
submitChanges()
{
this.$attrs.passDataBack(this, ['abc', 'xyz']) // passDataBack() is called, no issues.
},
...
Only problem is this is not available inside callback function.
So i can't do this.items = data.splice()
current context is available with arrow operator.
Correct version:
onsNavigatorProps: {
passDataBack: (data) => {
this.items = data.splice()
}
}