Watch Vuex state change not working as expected with v-model - javascript

I've created a global error state with Vuex, i'ts an array with objects of all current errors.
const store = new Vuex.Store({
state: {
errors: []
},
getters: {
getErrors: state => state.errors
},
mutations: {
setError: (state, message) => {
state.errors.push({ error: true, message });
},
removeError: (state, i) => {
state.errors.splice(i, 1);
}
}
});
I have a component that shows all the errors dynamically using Vuex state and what i'm trying to do is removing all the objects that have the error property set to false, the error property state is being handled by the setError mutation and the v-model property inside the component.
I'm trying to do that by watching for changes and removing the desired items from the array, but it is not removing right when the property changes to false, how can i achieve that?
Here is the live demo https://codesandbox.io/s/vue-template-h5hf7
<template>
<div id="snackbar">
<v-snackbar
v-for="(error, index) in getErrors"
:key="index"
v-model="error.error"
color="red"
:right="true"
:timeout="2000"
:top="true"
>
{{ error.message }}
<v-btn dark text #click="removeError(index)">Close</v-btn>
</v-snackbar>
</div>
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
export default {
name: "ErrorSnackbar",
computed: mapGetters(["getErrors"]),
methods: {
...mapMutations(["removeError"]),
removeError(i) {
this.$store.commit("removeError", i);
}
},
watch: {
getErrors: {
handler(newErrors) {
if (newErrors.length > 0) {
newErrors.forEach((error, i) => {
if (error.error === false) {
newErrors.splice(i, 1);
}
});
}
},
}
}
};
</script>

Your watcher will only respond to mutations of the array directly (such as an item being added or removed from it). In order to observe changes to the items within the array too, you need to use a deep watcher.
Also whenever you are looping over an array and removing items from the array at the same time, you should iterate in reverse order otherwise you will miss some elements.
watch: {
getErrors: {
deep: true,
handler(newErrors) {
for (let i = newErrors.length - 1; i >= 0; i--) {
if (!newErrors[i].error) {
newErrors.splice(i, 1)
}
}
}
}
}
Note that this may trigger another call to the handler since you're mutating the thing you are observing.
EDIT
Thanks for the codesandbox.
The issue has to do with <v-snackbar> not updating the model. I'm not completely sure how <v-snackbar> is implemented, but it seems that when the component is reused then its timeout gets cancelled and it will not emit an input event. Some of the components are getting reused as a result of adding and removing multiple errors at the same time.
What you need to do is to key each <v-snackbar> correctly to the same error object. Right now you have them keyed by the index in the array, but this will change as elements are removed from the array. So we have to come up with our own unique ID for each error object.
Here's an excerpt of the code changes you need to make:
// Define this at file-level
let nextKey = 1
mutations: {
setError: (state, message) => {
state.errors.push({
key: nextKey++,
error: true,
message,
})
}
}
<v-snackbar
v-for="error in getErrors"
:key="error.key"
>

Related

Vue 3.0 How to assign a prop to a ref without changing the prop

I'm sending from the parent component a prop: user. Now in the child component I want to make a copy of it without it changing the prop's value.
I tried doing it like this:
export default defineComponent({
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const user = ref(props.apiUser);
return { user };
}
});
But then if I change a value of the user object it also changes the apiUser prop. I thought maybe using Object.assign would work but then the ref isn't reactive anymore.
In Vue 2.0 I would do it like this:
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
return {
user: {}
}
},
mounted() {
this.user = this.apiUser;
// Now I can use this.user without changing this.apiUser's value.
}
};
Credits to #butttons for the comment that lead to the answer.
const user = reactive({ ...props.apiUser });
props: {
apiUser: {
required: true,
type: Object
}
},
setup(props) {
const userCopy = toRef(props, 'apiUser')
}
With the composition API we have the toRef API that allows you to create a copy from any source reactive object. Since the props object is a reactive, you use toRef() and it won't mutate your prop.
This is what you looking for: https://vuejs.org/guide/components/props.html#one-way-data-flow
Create data where you add the prop to
export default {
props: ['apiUser'],
data() {
return {
// user only uses this.apiUser as the initial value;
// it is disconnected from future prop updates.
user: this.apiUser
}
}
}
Or if you use api composition:
import {ref} from "vue";
const props = defineProps(['apiUser']);
const user = ref(props.apiUser);
You also may want to consider using computed methods (see also linked doc section from above) or v-model.
Please note that the marked solution https://stackoverflow.com/a/67820271/2311074 is not working. If you try to update user you will see a readonly error on the console. If you don't need to modify user, you may just use the prop in the first place.
As discussed in comment section, a Vue 2 method that I'm personally fond of in these cases is the following, it will basically make a roundtrip when updating a model.
Parent (apiUser) ->
Child (clone apiUser to user, make changes, emit) ->
Parent (Set changes reactively) ->
Child (Automatically receives changes, and creates new clone)
Parent
<template>
<div class="parent-root"
<child :apiUser="apiUser" #setUserData="setUserData" />
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of child component etc.)
export default {
data() {
apiUser: {
id: 'e134',
age: 27
}
},
methods: {
setUserData(payload) {
this.$set(this.apiUser, 'age', payload);
}
}
}
Child
<template>
<div class="child-root"
{{ apiUser }}
</div>
</template>
// ----------------------------------------------------
// (Obviously imports of components etc.)
export default {
props: {
apiUser: {
required: true,
type: Object
}
},
data() {
user: null
},
watch: {
apiUser: {
deep: true,
handler() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
}
}
},
mounted() {
// Whatever clone method you want to use
this.user = cloneDeep(this.apiUser);
},
methods: {
// Whatever function catching the changes you want to do
setUserData(payload) {
this.$emit('setUserData', this.user);
}
}
}
Apologies for any miss types

Deconstructing or pre-filtering data in a computed property in Vuejs

I need to put out a list of users based on whether they are active or not. I know I can separate the v-for and the v-if in nested divs but I can't because of the construct of the html. Also I need this for pagination purposes.
So I have my API containing thousands of users (some active true and same active false)
My template looks like this for the active ones:
<div v-if="filterformData.filterby === 'Active Operators'">
<div v-for="(user, key) in operators" :key="key" v-if="user.active">
User Name: {{user.name}}
User ID: {{user.id}}
...
</div>
</div>
Note: the first v-if it's checking for a dropdown select option of name Active Operators. That's in my data object.
My script relevant lines look like this
computed: {
...mapGetters("users", ["operators"])
},
methods: {
...mapActions("users", ["getUsers"])
}
created() {
this.getUsers();
}
My store relevant lines look like this:
import { axiosInstance } from "boot/axios";
export default {
namespaced: true,
state: {
operators: []
},
getters: {
operators: state => {
return state.operators;
}
},
actions: {
async getUsers({ commit }) {
const response = await axiosInstance.get("operator");
commit("setUsers", response.data.data);
}
},
mutations: {
setUsers: (state, operators) => (state.operators = operators)
}
};
Again, I'd like to contain the active users in a computed property that I can use for the loop. And eventually, I will be making another one for inactive users as well
Thanks
Just filter the active users in a computed property like this
computed: {
...
activeUsers() {
return this.operators.filter(user => user.active)
}
},
and change the v-for directive to this
<div v-for="(user, key) in activeUsers" :key="key">
and it should work without v-for and v-if on the same element.

Vue prop default value not working properly

Ok so I have the following prop that I get from the parent component
props: {
selectedExchange: {
default: 'acx',
}
},
And i try to use it in the following method
methods: {
getMarkets() {
const ccxt = require('ccxt')
const exchanges = ccxt.exchanges;
let marketPair = new ccxt[this.selectedExchange]()
let markets = marketPair.load_markets()
return markets
}
},
The expected result should be an array of markets for my project but i get an error in the console
[Vue warn]: Error in mounted hook: "TypeError: ccxt[this.selectedExchange] is not a constructor"
Now i thought it might be a problem with ccxt but it's not! I have tried the following code
methods: {
getMarkets() {
const ccxt = require('ccxt')
const exchanges = ccxt.exchanges;
let acx = 'acx'
let marketPair = new ccxt[acx]()
let markets = marketPair.load_markets()
return markets
}
},
If you don't see the change I have made a variable that contains 'acx' inside, exactly the same like the prop but this time it's created inside the method, and with this code I get the expected result, It has been bugging me for days and I can't seem to find an answer to it, did I initialize the default value wrong? When i look inside vue dev tools the value for my prop is array[0], only after I pass a value to that prop it gets updated, shouldn't i see the default value of acx in devtools? Any help is much appreciated!
Edit 1: Added parent component code
This is how i use the methods inside the parent and how my components are related to each other,
<div id="exchange">
<exchange v-on:returnExchange="updateExchange($event)"></exchange>
</div>
<div id="pair">
<pair :selectedExchange="this.selectedExchange"></pair>
</div>
And this is the code inside the script tags, i didn't include the import tag cause i don't think it would be useful
export default {
name: 'App',
components: { exchange, pair, trades },
data(){
return{
selectedExchange: ''
}
},
methods: {
updateExchange(updatedExchange){
this.selectedExchange = updatedExchange
}
},
};
In this case you will inherit the default value:
<pair></pair>
In this case you will always inherit the value of selectedExchange, even if it's null or undefined:
<pair :selectedExchange="this.selectedExchange"></pair>
So, in your case, you have to handle the default value on parent component.
This should work:
export default {
name: 'App',
components: { exchange, pair, trades },
data(){
return{
selectedExchange: 'acx' // default value
}
},
methods: {
updateExchange(updatedExchange){
this.selectedExchange = updatedExchange
}
},
};

How to get data resolved in parent component

I'm using Vue v1.0.28 and vue-resource to call my API and get the resource data. So I have a parent component, called Role, which has a child InputOptions. It has a foreach that iterates over the roles.
The big picture of all this is a list of items that can be selected, so the API can return items that are selected beforehand because the user saved/selected them time ago. The point is I can't fill selectedOptions of InputOptions. How could I get that information from parent component? Is that the way to do it, right?
I pasted here a chunk of my code, to try to show better picture of my problem:
role.vue
<template>
<div class="option-blocks">
<input-options
:options="roles"
:selected-options="selected"
:label-key-name.once="'name'"
:on-update="onUpdate"
v-ref:input-options
></input-options>
</div>
</template>
<script type="text/babel">
import InputOptions from 'components/input-options/default'
import Titles from 'steps/titles'
export default {
title: Titles.role,
components: { InputOptions },
methods: {
onUpdate(newSelectedOptions, oldSelectedOptions) {
this.selected = newSelectedOptions
}
},
data() {
return {
roles: [],
selected: [],
}
},
ready() {
this.$http.get('/ajax/roles').then((response) => {
this.roles = response.body
this.selected = this.roles.filter(role => role.checked)
})
}
}
</script>
InputOptions
<template>
<ul class="option-blocks centered">
<li class="option-block" :class="{ active: isSelected(option) }" v-for="option in options" #click="toggleSelect(option)">
<label>{{ option[labelKeyName] }}</label>
</li>
</ul>
</template>
<script type="text/babel">
import Props from 'components/input-options/mixins/props'
export default {
mixins: [ Props ],
computed: {
isSingleSelection() {
return 1 === this.max
}
},
methods: {
toggleSelect(option) {
//...
},
isSelected(option) {
return this.selectedOptions.includes(option)
}
},
data() {
return {}
},
ready() {
// I can't figure out how to do it
// I guess it's here where I need to get that information,
// resolved in a promise of the parent component
this.$watch('selectedOptions', this.onUpdate)
}
}
</script>
Props
export default {
props: {
options: {
required: true
},
labelKeyName: {
required: true
},
max: {},
min: {},
onUpdate: {
required: true
},
noneOptionLabel: {},
selectedOptions: {
type: Array
default: () => []
}
}
}
EDIT
I'm now getting this warning in the console:
[Vue warn]: Data field "selectedOptions" is already defined as a prop. To provide default value for a prop, use the "default" prop option; if you want to pass prop values to an instantiation call, use the "propsData" option. (found in component: <default-input-options>)
Are you using Vue.js version 2.0.3? If so, there is no ready function as specified in http://vuejs.org/api. You can do it in created hook of the component as follows:
// InputOptions component
// ...
data: function() {
return {
selectedOptions: []
}
},
created: function() {
this.$watch('selectedOptions', this.onUpdate)
}
In your InputOptions component, you have the following code:
this.$watch('selectedOptions', this.onUpdate)
But I am unable to see a onUpdate function defined in methods. Instead, it is defined in the parent component role. Can you insert a console.log("selectedOptions updated") to check if it is getting called as per your expectation? I think Vue.js expects methods to be present in the same component.
Alternatively in the above case, I think you are allowed to do this.$parent.onUpdate inside this.$watch(...) - something I have not tried but might work for you.
EDIT: some more thoughts
You may have few more issues - you are trying to observe an array - selectedOptions which is a risky strategy. Arrays don't change - they are like containers for list of objects. But the individual objects inside will change. Therefore your $watch might not trigger for selectedOptions.
Based on my experience with Vue.js till now, I have observed that array changes are registered when you add or delete an item, but not when you change a single object - something you need to verify on your own.
To work around this behaviour, you may have separate component (input-one-option) for each of your input options, in which it is easier to observe changes.
Finally, I found the bug. I wasn't binding the prop as kebab-case

Watching array stored in Vuex in VueJS

I have a customer list which is actually an array of objects. I store it in Vuex. I render the list in my component and each row has a checkbox. More precisely I use keen-ui and the checkbox rendering part looks like:
<tr v-for="customer in customers" :class="{ selected: customer.selected }">
<td>
<ui-checkbox :value.sync="customer.selected"></ui-checkbox>
</td>
<td>{{ customer.name }}</td>
<td>{{ customer.email }}</td>
</tr>
So the checkbox directly changes customers array which is bad: I use strict mode in Vuex and it throws me an error.
I want to track when the array is changed and call an action in order to change the vuex state:
watch: {
'customers': {
handler() {
// ...
},
deep: true
}
However it still changes the customer directly. How can I fix this?
First and foremost, be careful when using .sync: it will be deprecated in 2.0.
Take a look at this: http://vuex.vuejs.org/en/forms.html, as this problem is solved in here. Basically, this checkbox should trigger a vuex action on input or change. Taken from the docs:
<input :value="message" #input="updateMessage">
Where updateMessage is:
vuex: {
getters: {
message: state => state.obj.message
},
actions: {
updateMessage: ({ dispatch }, e) => {
dispatch('UPDATE_MESSAGE', e.target.value)
}
}
}
If you do not wish to track the mutations, you can move the state of this component away from vuex, to be able to use v-model in all its glory.
You just have to make a custom getter and setter:
<template>
<ui-checkbox :value.sync="thisCustomer"></ui-checkbox>
</template>
<script>
//this is using vuex 2.0 syntax
export default {
thisCustomer: {
get() {
return this.$store.state.customer;
},
set(val) {
this.$store.commit('SET_CUSTOMER', val);
// instead of performing the mutation here,
// you could also use an action:
// this.$store.disptach('updateCustomer')
}
},
}
</script>
In your store:
import {
SET_CUSTOMER,
} from '../mutation-types';
const state = {
customer: null,
};
const mutations = {
[SET_CUSTOMER](state, value) {
state.customer = value;
},
}
I'm not exactly sure what your store looks like, but hopefully this gives you the idea :)
if your customers are in the root state, you can try this:
watch: {
'$store.state.customers'{
handler() {
// ...
},
deep: true
}
}
try using mapState in your component and watch the customers like you have done above.worked for me

Categories