So, I am syncing a computed value to a component and setting it with a computed setter when it syncs back from the component.
My question is: Is it possible to replace a computed getter/setter with mapState and mapMutations or how would one achieve this in a more compact way?
<template>
<SomeComponent :value.sync="globalSuccess"></SomeComponent>
</template>
export default {
//...
computed: {
globalSuccess: {
get() {
return this.$store.state.globalSuccess;
},
set(val) {
this.$store.commit("globalSuccess", val);
}
}
}
}
I tried replacing it like this:
export default {
//...
computed: {
...mapState(["globalSuccess"]),
...mapMutations(["globalSuccess"]),
}
}
But unfortunately mapMutations(["globalSuccess"]) maps this.globalSuccess(value) to this.$store.commit('globalSuccess', value) according to the documentation of vuex.
But since my computed value gets set with globalSuccess = true internally through :value.sync in the template and not this.globalSuccess(true), globalSuccess will never be set to true.
Any idea how this could be possible? Or am I stuck using computed values with getter and setter?
So I just found out about this vuex module https://github.com/maoberlehner/vuex-map-fields which I installed as described on there:
// store.js
import { getField, updateField } from 'vuex-map-fields';
Vue.use(Vuex);
export default new Vuex.Store({
getters: {
getField,
//...
},
mutations: {
updateField,
//...
},
});
And then I made use of mapFields function:
// App.vue
export default {
//...
computed: {
...mapFields(["globalSuccess"]),
}
}
Which apparently dynamically maps to a computed setter and getter exactly as I wanted it:
export default {
//...
computed: {
globalSuccess: {
get() {
return this.$store.state.globalSuccess;
},
set(val) {
this.$store.commit("globalSuccess", val);
}
}
}
}
Here's a syntax I use:
export default {
//...
computed: {
globalSuccess: {
...mapState({ get: 'globalSuccess' }),
...mapMutations({ set: 'globalSuccess' }),
},
},
}
No additional dependencies needed. If you use it a lot, I suppose you could create a helper for it, but it is pretty neat as it is.
Related
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
Please check this example.
https://codepen.io/suedar/pen/XGyGOj
After I click the li, the div is required to display which is not.
I know it is because Reactivity in Depth.
I want to know how can I monitor a deep value in object while using vuex. Thanks for ur answer.
Deep watch should work on Vuex store as well.
import { mapState, mapMutations } from 'vuex';
export default {
computed: {
...mapState([
'allCategory'
])
},
methods: {
...mapMutations([
'CHANGE_ALL_CATEGORY'
]),
toggleTab(index, isShow) {
// ...some action
this.CHANGE_ALL_CATEGORY(allCategory);
},
},
watch: {
allCategory: {
handler(val) {
this.CHANGE_ALL_CATEGORY(val);
},
deep: true
}
}
}
I have a props that i want to use to make a dynamic mapGetters but the the mapGetters sees the props as undefined, probably because the computed is loaded before the props. Do anybody know how i can make it dynamic? my code is as follow:
export default {
props: ['listType'],
components: {
addrow: AddRow
},
computed: {
...mapGetters({
list: `${this.listType}/list`,
current: 'Dropdown/current'
})
},
}
[UPDATE]
I have found the solution thanks to #boussadjrabrahim
My working code look like this:
export default {
props: ['listType'],
components: {
addrow: AddRow
},
computed: {
...mapGetters({
current: 'Dropdown/current'
}),
...mapState({
list (state, getters) {
return getters[`${this.listType}/list`]
}
})
}
}
You can also use this.$store for complete access to the store. So, list would become
export default {
props: ['listType'],
computed: {
list() {
return this.$store.getters[`${this.listType}/list`]
}
}
}
Use the dispatch method to trigger an action, like so
export default {
props: ['listType'],
methods: {
sortList(order) {
this.$store.dispatch(`${this.listType}/list`, order)
}
}
}
What I found that worked was essentially rolling your own mapGetters method in the created() stage of the lifecycle.
Note that this solution has NOT been fully vetted and I have no idea what, if any "gotchas" it may create. As always, caveat emptor.
export default {
props: ['listType'],
components: {
addrow: AddRow
},
created() {
// This can be configured a number of ways, but for brevity:
const mappableGetters = {
list: `${this.listType}/list`,
current: 'Dropdown/current'
}
Object.entries(mappableGetters).forEach(([key, value]) => {
// Dynamically defines a new getter on `this`
Object.defineProperty(this, key, { get: () => { return this.$store.getters[value] } })
})
// Now you can use: `this.list` throughout the component
},
computed: {
// Roll your own mapper above in `created()`
// ...mapGetters({
// list: `${this.listType}/list`,
// current: 'Dropdown/current'
// })
}
}
I have a simple component that uses mixin that's shared across multiple components with similar functionality.
When I run it I seem to be getting
Property or method "activeClass" is not defined on the instance but
referenced during render.
Here's my mixin
<script>
export default {
data() {
return {
opened: false,
identity: ''
}
},
computed: {
activeClass() {
return {
active: this.opened
};
}
},
created() {
window.EventHandler.listen(this.identity + '-toggled', opened => this.opened = opened);
},
methods: {
toggle() {
window.EventHandler.fire('toggle-' + this.identity);
}
}
}
</script>
and my component
<template>
<span class="pointer" :class="activeClass" #click="toggle"><i class="fas fa-search"></i></span>
</template>
<script>
import Trigger from '../../mixins/Trigger';
export default {
data() {
return {
mixins: [Trigger],
data() {
return {
identity: 'language'
}
}
}
}
}
</script>
For some reason I cannot seem to be able to access activeClass computed property from within the component. Any idea why is this happening?
Try to move mixin to components main scope. Not in data function rerurn
I try to explain it as simple as possible. I have something like this. Simple Vue root, Vuex store and input with v-model inside navbar id.
That input is not reactive... Why?!
HTML
<div id="navbar">
<h2>#{{ test }}</h2>
<input v-model="test" />
</div>
store.js
import Vuex from 'vuex'
export const store = new Vuex.Store({
state: {
test: 'test'
},
getters: {
test (state) {
return state.test
}
}
})
Vue Root
import { store } from './app-store.js'
new Vue({
el: '#navbar',
store,
computed: {
test () {
return this.$store.getters.test
}
}
})
You're binding to a computed property. In order to set a value on a computed property you need to write get and set methods.
computed:{
test:{
get(){ return this.$store.getters.test; },
set( value ){ this.$store.commit("TEST_COMMIT", value );}
}
}
And in your store
mutations:{
TEST_COMMIT( state, payload ){
state.test=payload;
}
}
Now when you change the value of the input bound to test, it will trigger a commit to the store, which updates its state.
You don't want to use v-model for that. Instead, use #input="test" in your input field and in the your methods hook:
test(e){
this.$store.dispatch('setTest', e.target.value)
}
Inside your Vuex store:
In mutations:
setTest(state, payload){
state.test = payload
},
In actions:
setTest: (context,val) => {context.commit('setTest', val)},
The input should now be reactive and you should see the result in #{{test}}
Here is an example of how I handle user input with Vuex: http://codepen.io/anon/pen/gmROQq
You can easily use v-model with Vuex (with actions/mutations firing on each change) by using my library:
https://github.com/yarsky-tgz/vuex-dot
<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'])
.commit('editUser') // payload example: { name: 'Peter'}
.map()
}
}
</script>
The computed property is one-way. If you want two-way binding, make a setter as the other answers suggest or use the data property instead.
import { store } from './app-store.js'
new Vue({
el: '#navbar',
store,
// computed: {
// test() {
// return this.$store.getters.test;
// }
// }
data: function() {
return { test: this.$store.getters.test };
}
});
But a setter is better to validate input value.