In my Model.vue component (in a Nuxt.js and Vuetifyjs application) I have this piece of code:
<v-checkbox v-model="selected"></v-model>
In the script section of this component, I have:
computed: {
selected: {
get() {
return this.$store.state.selected
},
set(value) {
this.$store.commit('UPDATE_SELECTED', value)
}
},
},
In the store, I have this:
mutations: {
UPDATE_SELECTED: (state, value) => {
state.selected.push(value)
}
}
And the state of this store contains the selected entry as follows:
state: {
selected: []
}
AFAIK, I complied to the documentation, but when I click on the v-checkbox component, it does not check/uncheck. What is wrong?
The problem occurs because you're using an array for modeling the boolean state property named selected.
So, according to your code, at mutations side, on each mutation you're pushing a new boolean value in the latest position of an array named selected.
Also, at get side, the get() function of your computed property returns the entire array as property to be displayed, resulting in an unchecked checkbox at client side.
So, assuming you need to deal with multiple checkboxes, in order to make the current one working properly you could write it as follows:
Vuex Store
let store = new Vuex.Store({
state: {
selected: []
},
mutations: {
UPDATE_SELECTED: (state, value) => {
//mutating the 0th array position of the selected property
state.selected[0] = value
}
}
})
Computed property
computed: {
selected: {
get() {
//getting the 0-index element of the array, assuming this checkbox is the 0th
return this.$store.state.selected[0]
},
set(value) {
this.$store.commit('UPDATE_SELECTED', value)
}
}
}
Here you find a working fiddle.
Alternatively, if you need just to deal with a single checkbox in your application state, it'd be enough to model the state.selected property as a boolean. I mean, it would suffice to modify your Vuex state as follows:
state: {
selected: true //or false if you expect unchecked checkbox as default
}
Related
I have a button component that looks like this
<template>
<button
class="o-chip border-radius"
:class="{
'background-color-blue': theValue.isSelected,
'background-color-light-grey': !theValue.isSelected,
}"
#click="onSelection(theValue)"
>
{{ theValue.displayText }}
</button>
</template>
And when it is pressed it sets it isSelected state and emit an event to the parent component
private onSelection() {
this.theValue.isSelected = !this.theValue.isSelected;
this.$emit('selected', this.theValue);
}
So far so good the issue is in the parent component. Here I go through all the items that is the array that creates the button components above. The .value property is the unique identifier here so what I do is I look through the array with the item sent from the button component, then when found i use splice to trigger reactivity to replace that array object with the one sent from the button component. When i console.log this it works it shows that isSelected property now is true.
private selectedItem(item: Item) {
var index = this.itemSelectOptions
.map((p) => p.value)
.indexOf(item.value);
this.itemSelectOptions.splice(index, 1, item);
console.log(this.itemSelectOptions);
}
But then i have this get method that checks for anyChanges on this array and other things and then render UI based on true/false here. The issue is that when this array get changed the anyChanges method does not react and remains false.
private get anyChanges(): boolean {
console.log(this.itemSelectOptions);
return this.itemSelectOptions!.some((p) => p.isSelected);
}
How do i make it so that the anyChanges get method reacts on the changes made to itemSelectOptions which is also an get function
private get itemSelectOptions(): Item[] {
return this.items
? this.items.map((item) => ({
value: item.id.toString(),
displayText: item.displayName.toString(),
isSelected: false,
}))
: [];
}
What you want is a watcher on itemSelectOptions
watch: {
question(newOptions, oldOptions) {
this.anyChanges(newOptions)
}
},
guess it will look smthing like this ^^
https://vuejs.org/guide/essentials/watchers.html#basic-example
The reason this was not working was that since itemSelectOptions where a get function it should/can not be modified. So changing it into a private variable and initializing it solved it like this.
private itemSelectOptions: Item[] = [];
and then initializing
this.itemSelectOptions = this.items
? this.items.map((item) => ({
value: item.id.toString(),
displayText: item.displayName.toString(),
isSelected: false,
}))
: [];
I'm sorry if this is a duplicate. I have searched around for this information but I'm not really sure how best to describe it so finding it from google searches has proved very difficult.
I write Vue applications a lot and I frequently run into the following situation where I have a complex object which I would like to pass into a child component via props so that I can encapsulate its mutation nicely and then have those changes mirrored in the parent component so that all of that information is in one place.
Consider the following simple example.
// parent.vue
// list of complex objects in data
export default {
data() {
return {
tickets: [
{ name: "ticket 1", price: 20, quantity: 0 },
{ name: "ticket 2", price: 20, quantity: 0 },
// etc
]
}
}
}
// child.vue
// list passed from parent into props
export default {
props: {
tickets: Array
}
}
To obey the Vue one-way data flow best practices, I should create a local copy of the tickets prop in child. I want the local copy to be updated if the parent mutates the tickets list for any reason, so I watch the tickets prop for changes and update the local copy accordingly:
// child.vue
export default {
props: {
tickets: Array
},
data() {
return {
_tickets: []
}
},
watch: {
tickets: {
handler: function (value) {
// make deep local copy
this._tickets = clone(value);
},
deep: true,
immediate: true
}
}
}
So now we have a local deep copy that we can make changes to without violating Vue's one-way data flow. I want all of my local changes to be reflected in the parent so I deep watch the local copy and emit the changed value back up to the parent. Props down, events up. Now my child component looks like this:
// child.vue
export default {
props: {
tickets: Array
},
data() {
return {
_tickets: []
}
},
watch: {
tickets: {
handler: function (value) {
// make deep local copy
this._tickets = clone(value);
},
deep: true,
immediate: true
},
_tickets: {
handler: function () {
this.$emit('update:tickets', this._tickets);
},
deep: true
}
}
}
Now we run into an issue. This is an infinite loop because the tickets watcher mutates the _tickets property, invoking its watcher and mutating the parent value, invoking the tickets watcher... and so on. It might be possible (untested) to work around this issue by setting a flag on the ticket prop watcher so that the loop gets caught before re-emitting back up to the parent, but then this seems like a whole lot of unnecessary resources spent on creating copies just to conform with Vue best practices.
What is considered the correct approach to mutating complex object props?
I am facing an issue where I have some template HTML in a component that relies on the computed getter of a Vuex method. As you can see in the template, I am simply trying to show the output of the computed property in a <p> tag with {{ getNumSets }}.
As I update the state with the UPDATE_EXERCISE_SETS mutation, I can see in the Vue devtools that the state is updated correctly, but the change is not reflected in the <p> {{ getNumSets }} </p> portion.
Template HTML:
<template>
...
<v-text-field
v-model="getNumSets"
placeholder="S"
type="number"
outlined
dense
></v-text-field>
<p>{{ getNumSets }}</p>
...
</template>
Component Logic:
<script>
...
computed: {
getNumSets: {
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
...
</script>
Vuex Store Logic:
...
state: {
designerBucket: []
},
getters: {
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
state.designerBucket.forEach(exercise => {
if (exercise.id == payload.id) {
exercise.numSets = payload.numSets
}
})
}
}
Any insight is very appreciated!
P.S. I have also tried using a for (var i=0...) loop, looping over the indices and then using Vue.set() to set the value. This did update the value in the store as well, but the computed property is still not updating the template.
This turned into a bit of a long-winded answer, but bear with me.
Here's my hunch: since you're returning a function from your Vuex getter, Vue isn't updating your computed property on state changes because the function never changes, even if the value returned from it would. This is foiling the caching mechanism for computed properties.
Reactivity for Arrow Function Getters
One of the things to keep in mind when creating a getter like this, where you return an arrow function:
getNumSetsForExercise: (state) => (payload) => {
var numSets = 0
for (var i = 0; i < state.designerBucket.length; i++) {
if (state.designerBucket[i].id == payload.id) {
numSets = state.designerBucket[i].numSets
}
}
return numSets
}
...is that you're no longer returning actual state data from your getter.
This is great when you're using it to pull something from state that depends on data that's local to your component, because we don't need Vue to detect a change, we just need the function to access current state, which it does fine.
BUT, it may also lead to the trap of thinking that updating state should update the getter, when it actually doesn't. This is really only important when we try to use this getter in a computed property like you have in the example, due to how computed properties track their dependencies and cache data.
Computed Caching and Dependency Detection
In Vue, computed properties are smarter than they first seem. They cache their results, and they register and track the reactive values they depend on to know when to invalidate that cache.
As soon as Vue calculates the value of a computed property, it stores it internally, so that if you call the property again without changing dependencies, the property can return the cached value instead of recalculating.
The key here for your case is the dependency detection– your getter has three dependencies that Vue detects:
get() {
var numSets = this.$store.getters['designer/getNumSetsForExercise']({id: this.id, parent: this.parent})
return numSets
},
The getter: this.$store.getters['designer/getNumSetsForExercise']
this.id
this.parent
None of these values change when <v-text-field> calls your setter.
This means that Vue isn't detecting any dependency changes, and it's returning the cached data instead of recalculating.
How to Fix it?
Usually, when you run into these sorts of dependency issues, it's because the design of the state could be improved, whether by moving more data into state, or by restructuring it in some way.
In this case, unless you absolutely need designerBucket to be an array for ordering purposes, I'd suggest making it an object instead, where each set is stored by id. This would simplify the implementation by removing loops, and remove the need for your getter altogether:
...
state: {
designerBucket: {}
},
mutations: {
UPDATE_EXERCISE_SETS(state, payload) {
// Need to use $set since we're adding a new property to the object
Vue.set(state.designerBucket, payload.id, payload.numSets);
}
}
Now, instead of invoking a getter, just pull designerBucket from state and access by this.id directly:
<script>
...
computed: {
getNumSets: {
get() {
return this.$store.state.designerBucket[this.id];
},
set(value) {
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
});
}
}
...
</script>
This should allow Vue to detect changes correctly now, and prevent the stale cache problem from before.
Edited: First import mapGetters from 'vuex' like this on the top of the script tag.
import { mapGetters } from "vuex"
Now in your computed object, add mapGetters and pass arguments to the getter method inside the get() method like this:-
computed: {
...mapGetters('designer',['getNumSetsForExercise']),
getNumSets: {
get() {
var numSets = this.getNumSetsForExercise({id: this.id, parent: this.parent})
return numSets
},
set(value) { // This correctly updates the state as seen in the Vue DevTools
this.$store.commit('designer/UPDATE_EXERCISE_SETS', {
id: this.exerciseId,
parentName: this.parent,
numSets: parseInt(value),
date: this.date
})
}
}
And see if it works.
i want to watch when a mutation called and updated a status. i make a component to show database table count when api called.
this is my store i wrote
const state = {
opportunity: ""
}
const getters = {
countOpportunity: state => state.opportunity
}
const actions = {
// count opportunity
async totalOpportunity({ commit }) {
const response = await axios.get(count_opportunity)
commit("setOpportunity", response.data)
},
}
const mutations = {
setOpportunity: (state, value) => (state.opportunity = value)
}
i want to show this getter value when this mutation called in another component name Opportunity.vue file.
i showed database count values in file name Dashboard.vue
i wrote it like this.
computed: {
...mapGetters(["countOpportunity"])
},
watch: {},
mounted() {
//do something after mounting vue instance
this.$store.watch(() => {
this.$store.getters.countOpportunity;
});
},
created() {
this.totalOpportunity();
},
methods: {
...mapActions(["totalOpportunity"])
}
and showed my view like this.
<div class="inner">
<h3>{{ countOpportunity }}</h3>
<p>Opportunities</p>
</div>
when api called and count increase shows my mutations. but my view value not updated (countOpportunity). any one can help me to fix this.
The issue here (most likely) is that the value of response.data is an object or an array. You've initially defined opportunity as '' which is not an observable object or array. You have 2 choices:
Redefine it as an empty object or array, depending on the response:
opportunity: [] // or {}
Otherwise, use Vue.set() to apply reactivity when changing it:
(Vue.set(state, 'opportunity', value))
I cant get Two-way Computed Property in combination with vuex to work.
If there are input changes I want to set getIsUnsavedData to true and "copy"/commit the changes into a new variable $store.state.authenticatedUser.changedData. After there is any change I want the input to get() its value from $store.state.authenticatedUser.changedData instead of $store.state.authenticatedUser.data to display the change.
At fist everything works like expected. If there are input changes, the changed value will be replicated in the $store.state.authenticatedUser.changedData property. The getIsUnsavedData value changes to true and the get() points to the replicated data.
There is only one bug left. Suddenly the computed property never changes although the vuex store is updating correctly. The set() function still gets called, but the get() doesn't .
<ui-textbox #change="valueChanged" v-model="userName"></ui-textbox>
// ...
computed: {
userName: {
get() {
return this.$store.getters.getIsUnsavedData ? this.$store.state.authenticatedUser.changedData.name : this.$store.state.authenticatedUser.data.name
},
set(value) {
this.$store.commit('setChangedUserData', {
key: 'name',
value: value
})
}
}
},
methods: {
valueChanged() {
this.$store.commit('setUnsavedState', true)
}
},
// ....
Try to use mine library
https://github.com/yarsky-tgz/vuex-dot
it's done for such situations, to make code footprint of setters/getters in your code much less.
<template>
<form>
<input v-model="name"/>
<input v-model="email"/>
</form>
</template>
<script>
import { takeState } from 'vuex-dot';
export default {
computed: {
...takeState('user')
.expose(['name', 'email'])
.dispatch('editUser')
.map()
}
}
</script>
(updated to reflect Andor's input)
v-model can point to a computed property but it needs to handle work a bit differently than when it just refers to data.
vue's doc
applying the section on two-way computed properties:
<ui-textbox v-model="userName"></ui-textbox>
// ...
computed: {
userName: {
get () {
return this.$store.state.obj.userName
},
set (value) {
this.$store.commit('updateUserName', value)
}
}
}
I might be missing something about what you're doing but this is how I'd start to solve the problem.