Hello I am beginner in Vue and I do have a problem that's really bugging me.
I am wondering should we use v-model directive to modify vuex store? Vuex says that we should modify vuex store only by mutations but v-model makes everything easier and shorter.(I am asking because i couldn't find clear answer)
https://vuex.vuejs.org/guide/forms.html
When using Vuex in strict mode, it could be a bit tricky to use v-model on a piece of state that belongs to Vuex.
The "Vuex way" to deal with it is binding the <input>'s value and call an action on the input or change event.
Be sure to check out the simple "Two-way Computed Property" example on that page:
<input v-model="message">
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
I think another good option which hasn't been mentioned in any answer here is to use vuex-map-fields. In fact, the library author has written a very nice explanation for the library's usefulness. As per its GitHub page, to use the library you can do something like this:
In your Vuex Store, you can have a snippet similar to this:
import Vue from 'vue';
import Vuex from 'vuex';
import { getField, updateField } from 'vuex-map-fields';
Vue.use(Vuex);
export default new Vuex.Store({
// ...
modules: {
fooModule: {
namespaced: true,
state: {
foo: '',
},
getters: {
getField,
},
mutations: {
updateField,
},
},
},
});
And in your component code, you can have something along the lines of this:
<template>
<div id="app">
<input v-model="foo">
</div>
</template>
<script>
import { mapFields } from 'vuex-map-fields';
export default {
computed: {
// `fooModule` is the name of the Vuex module.
...mapFields('fooModule', ['foo']),
},
};
</script>
Additional examples for various use cases are shown in the library's GitHub repository that I linked to in the first sentence of this answer.
Above solution can also implemented with mutations:
<template>
<input v-model="message">
</template>
<script>
import { mapMutations, mapState } from 'vuex';
export default {
computed: {
...mapState({messageFromStore: 'message'}),
message: {
get() {
return this.messageFromStore;
},
set(value) {
this.updateMessage(value);
}
}
},
methods: {
...mapMutations('updateMessage')
}
};
</script>
My Solution to this was to use a getter to set value and #input to call the mutation.
<input
type="text"
:value="$store.getters.apartmentStreet"
#input="value => $store.commit('apartmentValue', { handle: 'street', value })"
>
getters.js:
export default {
apartmentStreet: state => state.apartment.street,
};
mutations.js
export default {
apartmentValue(state, payload) {
let oldValue = state.apartment[payload.handle];
let newValue = payload.value;
if (newValue !== oldValue) state.apartment[payload.handle] = payload.value;
}
};
If you use this method be sure to check which event you want.
I use this solution.
data() {
return {
formData: {
username: '',
email: '',
bio: {
firstName: '',
lastName: ''
},
games: ['civ4', 'caesar3', 'homeworld', 'cataclysm'],
}
}
},
computed: {
...mapGetters({ //or mapState
user: 'users'
})
},
watch: {
user(newValue) {
this.formData.username = newValue.name;
this.formData.email = newValue.email;
this.formData.bio.firstName = newValue.bio.firstName;
this.formData.bio.lastName = newValue.bio.lastName;
this.formData.games = newValue.games.map(x=> { return x });
}
},
beforeCreate: fucntion() {
this.$store.dispatch('getUser');
}
And then you just regularly use v-model.
It is important to make deep copy of object from store, like using map for array, and how i did stuff with object inside.
And, also you need to have initiated this user object in store also, with empty fields.
Yes you can but is not the best practice.
As the documentation say the state should be updated only inside mutation to keep the control over the state.
But if you really want to do it you can with:
v-model="$store.state.yourProperty"
Related
I'm struggling with how Vue updates props/child components.
Suppose the following component:
<template>
<v-card>
<Modification v-model="newObject"></Modification>
<OtherComponent #close="resetObject"></OtherComponent>
</v-card>
</template>
<script>
import { MyClass } from "classes";
import Modification from "b";
import OtherComponent from "a";
export default {
name: "MyForm",
components: { OtherComponent, Modification },
props: {
existingObject: {
type: [MyClass, typeof undefined],
required: false,
default: undefined
}
},
data() {
return {
newObject: undefined
};
},
created() {
this.newObject =
this.existingObject !== undefined
? this.existingObject.clone()
: new MyClass();
},
methods: {
resetObject() {
this.newObject =
this.existingObject !== undefined
? this.existingObject.clone()
: new MyClass();
}
}
};
</script>
How MyClass is defined:
export class MyClass {
constructor({ a= null, b=null} = {}) {
this.a = a;
this.b = b;
}
toPayload(){
return { a:this.a , b:this.b };
}
clone() {
return new MyClass(this.toPayload());
}
}
This component receives an existing class instance of MyClass, clones it (clone => new MyClass(...)) and passes it to the Modification component which does some modification upon user input. So far so good, the modification works. However once the customEvent is fired and the resetObject method is called the newObject is reset but the Modification component is not updated with the now reset newObject - it still displays the old, modified values. I also checked inside the Modification component wether or not the update happens: It doesn't.
Why is this the case? Am I missing a step? Am I not aware of a Vue specific mechanism?
Note: I found this blog which provides solutions to force the Modificationcomponent to update. For now it seems to hacky for me to be "THE" solution.
Thanks in advance.
EDIT:
Adding a computed property which includes a console.log(JSON.stringify(this.newObject)) fires everytime newObject is updated.
Also adding a <span> {{ newObject.a }} </span> to the template updates evertime.
Both these tests convince me that the variable not only should be but actually IS reactive.
EDIT 2:
The Modification component consists, for now, of 2 Input components.
It looks like this.
<template>
<v-card-text>
<ModifyA v-model="object.a" #input="handleInput" />
<ModifyB v-model="object.b" #input="handleInput" />
</v-card-text>
</template>
<script>
import { MyClass } from "classes";
import ModifyA from "...";
import ModifyB from "...";
export default {
name: "ShiftFormFields",
components: { ModifyA, ModifyB },
props: {
value: {
type: MyClass,
required: true
}
},
data() {
return { object: this.value };
},
methods: {
handleInput() {
this.$emit("input", this.object);
}
}
};
</script>
If I try adding the ModifyA Input into the component instead of the Modification component like this
<template>
<v-card>
<ModifyA v-model="newObject.a"></Modification>
<OtherComponent #close="resetObject"></OtherComponent>
</v-card>
</template>
the resetObject also resets the value shown in the ModifyA component.
You didn't show how MyClass clones your object.
I'm guessing something in there isn't reactive.
You can check by doing console.log() and see what it says on the console.
If it's reactive, it should show something like MyClass {__ob__: Observer}
You can probably use this.$set('propName', value) to fix your problem
Docs: https://v2.vuejs.org/v2/api/#vm-set
Adds a property to a reactive object, ensuring the new property is also reactive, so triggers view updates. This must be used to add new properties to reactive objects, as Vue cannot detect normal property additions (e.g. this.myObject.newProperty = 'hi').
Either there is a typo in your post, or the typo also exists in your code and is the source of your problem.
In your post you're binding "newObjekt" to the Modification component, but your parent component has the property "newObject"
is this the source of your issue?
I found the solution in this answer.
As I edited my original post with the definition of the Modification component
<template>
<v-card-text>
<ModifyA v-model="object.a" #input="handleInput" />
<ModifyB v-model="object.b" #input="handleInput" />
</v-card-text>
</template>
<script>
import ModifyA from "...";
import ModifyB from "...";
export default {
name: "ShiftFormFields",
components: { ModifyA, ModifyB },
props: {
value: {
type: MyClass,
required: true
}
},
data() {
return { object: this.value };
},
methods: {
handleInput() {
this.$emit("input", this.object);
}
}
};
</script>
it shows the "problem" why the Fields ModifyA and ModifyB do not update if the value updates in the parent component.
As seen in the above definition the variable object is only set to the value once the Component is initialized. It follows that object is not reactive on behalf of value.
To solve this one can use the approach of the above mentioned answer:
<template>
<v-card-text>
<ModifyA v-model="object.a" />
<ModifyB v-model="object.b" />
</v-card-text>
</template>
<script>
import { Shift } from "classes";
import ModifyA from "...";
import ModifyB from "...";
export default {
name: "ShiftFormFields",
components: { ModifyA, ModifyB },
props: {
value: {
type: MyClass,
required: true
}
},
data() {
return { object: this.value };
},
watch: {
value(val) {
this.object = val;
},
object(value) {
this.$emit("input", value);
}
}
};
</script>
Due to the watcher, the object variable is updated whenever the value get's updated by the parent.
I have a following store module:
const state = {
user: {}
}
const mutations = {
saveData(state, payload) {
state.user = payload
}
}
const actions = {
async loadData({ commit }, payload) {
try {
const res = await axios.get('https://api.example.com/user/1')
commit('saveData', res.data.data)
} catch(e) {
console.log(e)
}
}
}
const getters = {
getData(state) {
return state.user
}
}
Now what's the best way to save the data in component? Is it using watch
import { mapGetters } from 'vuex'
export default {
data() {
return {
user: {}
}
},
computed: {
...mapGetters({
getData
})
},
watch: {
'$store.state.users.users'() {
this.user = this.getData
}
}
}
... or store.subscribe?
import { mapGetters } from 'vuex'
export default {
data() {
return {
user: {}
}
},
computed: {
...mapGetters({
getData
})
},
created() {
this.$store.subscribe((mutation, state) => {
if(mutation.type === 'saveData') {
this.user = this.getData
}
})
}
}
Since you already know about store mapping I suppose you try to have some kind of edit form where you need the actual data taken from the database and also the ability to change this data to later send it back to the database.
You don't need a getter to have a simple reference to a store item. You will be very fine with mapState in your component:
{
computed: {
...mapState({
user: state => state.user,
}),
}
}
So as soon as user changed in the store your component will know about it. And here you can update the object you're editing. Let's rename it to edit to avoid collision:
{
data() {
return {
edit: {},
}
},
computed: {
...mapState({
user: state => state.user,
}),
},
watch: {
user: {
immediate: true,
handler(user) {
this.edit = { ...user }
},
},
},
}
Now edit is updated accordingly even if the component was mounted after the store item was updated (thanks to immediate option), and you can safely modify it in your component without any impact on the store reference.
P.S. Have to mention that in this implementation if you want to have reactivity on fields within edit object, then you need to update the whole edit object on each it's field update like this: this.edit = {...this.edit, [prop]: value}. But if you want it to be the natural Vue way, then first you need to initialize edit with actual object structure, and in the watcher for user perform something like Object.assign(this.edit, user).
It's preferable to use computed properties to access store data and keep it reactive.
It's possible to create a computed property using mapGetters as you do in the shared snipped, however, taking into account the getter is simply returning user from state, I don't think you need a getter at all, you can simply map the value from state by using mapState helper. In this way the component would be simplified to something like as follows:
import { mapState } from 'vuex'
export default {
computed: {
...mapState([
'user'
])
}
}
With the above approach you can reference user as this.user in the component's methods or simply as user in template of the component. Also, since the getter is out of use, you can delete the getter definition from the store (unless you are using it anywhere else).
My goal is to create an 'edit account' form such that a user can modify their account data. I want to present the account data in a form that is already filled with the users data i.e username, email, address ...
The user can then modify the data in the form and submit this form that will update their user information.
I am using v-model to bind the form input to an object called accountInfo in my data, that looks like this:
data() {
return {
accountInfo: {
firstName: ''
}
}
}
And here is an example of a form input in my template:
<input v-model.trim="accountInfo.firstName" type="text" class="form-control" id="first-name" />
The values for the key's in the object are currently empty strings but I would like the values to come from an object called userProfile that is a state property in vuex.
In my 'edit account' component I am mapping the vuex state by importing:
import { mapState } from "vuex";
then using the following in a computed property
computed: {
...mapState(["userProfile"])
}
What I would like to do is instead of having empty strings as the values of accountInfo, assign them values from the userProfile computed property mapped from vuex, like so:
data() {
return {
accountInfo: {
firstName: this.userProfile.fristName,
}
}
}
This will provide the desired initial data for my form but unfortunately this doesn't work, presumably because data is rendered earlier on in the life cycle than computed properties.
Full code:
EditAccount.vue
<template>
<div class="container-fluid">
<form id="sign_up_form" #submit.prevent>
<div class="form-row">
<div class="form-group col-md-6">
<input v-model.trim="signupForm.firstName" type="text" class="form-control" id="first_name" />
</div>
</div>
</form>
</div>
</template>
<script>
import { mapState } from "vuex";
import SideBar from "../common/SideBar.vue";
export default {
name: "EditAccount",
computed: {
...mapState(["userProfile"])
},
data() {
return {
accountInfo: {
firstName: this.userProfile.firstName
}
};
}
};
</script>
store.js:
export const store = new Vuex.Store({
state: {
userProfile: {firstName: "Oamar", lastName: "Kanji"}
}
});
You were right, computeds are evaluated after the initial data function is called.
Quick fix
In the comments, #Jacob Goh mentioned the following:
$store should be ready before data function is called. Therefore, firstName: this.$store.state.userProfile.firstName should just work.
export default {
name: 'EditAccount',
data() {
return {
accountInfo: {
firstName: this.$store.state.userProfile.firstName
}
}
}
};
Really need computeds?
See #bottomsnap's answer, where setting the initial value can be done in the mounted lifecycle hook.
With your code, it would look like this:
import { mapState } from 'vuex';
export default {
name: 'EditAccount',
computed: {
...mapState(['userProfile'])
},
data() {
return {
accountInfo: {
firstName: ''
}
}
}
mounted() {
this.accountInfo.firstName = this.userProfile.firstName;
}
};
Though it may render once without the value, and re-render after being mounted.
Container versus presentation
I explain Vue's communication channels in another answer, but here's a simple example of what you could do.
Treat the Form component as presentation logic, so it doesn't need to know about the store, instead receiving the profile data as a prop.
export default {
props: {
profile: {
type: Object,
},
},
data() {
return {
accountInfo: {
firstName: this.profile.firstName
}
};
}
}
Then, let the parent handle the business logic, so fetching the information from the store, triggering the actions, etc.
<template>
<EditAccount :profile="userProfile" :submit="saveUserProfile"/>
</template>
<script>
import { mapState, mapActions } from "vuex";
export default {
components: { EditAccount },
computed: mapState(['userProfile']),
methods: mapActions(['saveUserProfile'])
}
</script>
While Jacob is not wrong saying that the store is ready, and that this.$store.state.userProfile.firstName will work, I feel this is more a patch around a design problem that can easily be solved with the solution above.
Bind your input with v-model as you were:
<div id="app">
<input type="text" v-model="firstName">
</div>
Use the mounted lifecycle hook to set the initial value:
import Vue from 'vue';
import { mapGetters } from 'vuex';
new Vue({
el: "#app",
data: {
firstName: null
},
computed: {
...mapGetters(["getFirstName"])
},
mounted() {
this.firstName = this.getFirstName
}
})
I'm trying to pass the store module namespace via props to a component. When I try and map to getters with the prop, it throws this error,
Uncaught TypeError: Cannot convert undefined or null to object
If I pass the name as a string it works.
This Works
<script>
export default {
props: ['store'],
computed: {
...mapGetters('someString', [
'filters'
])
}
}
</script>
This does not work
this.store is defined
this.store typeof is a String
<script>
export default {
props: ['store'],
computed: {
...mapGetters(this.store, [
'filters'
])
}
}
</script>
I used this style utilising beforeCreate to access the variables you want, I used the props passed into the component instance:
import { createNamespacedHelpers } from "vuex";
import module from '#/store/modules/mymod';
export default {
name: "someComponent",
props: ['namespace'],
beforeCreate() {
let namespace = this.$options.propsData.namespace;
const { mapActions, mapState } = createNamespacedHelpers(namespace);
// register your module first
this.$store.registerModule(namespace, module);
// now that createNamespacedHelpers can use props we can now use neater mapping
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
}),
// because we use spread operator above we can still add component specifics
aFunctionComputed(){ return this.name + "functions";},
anArrowComputed: () => `${this.name}arrows`,
};
// set up your method bindings via the $options variable
this.$options.methods = {
...mapActions(["initialiseModuleData"])
};
},
created() {
// call your actions passing your payloads in the first param if you need
this.initialiseModuleData({ id: 123, name: "Tom" });
}
}
I personally use a helper function in the module I'm importing to get a namespace, so if I hadmy module storing projects and passed a projectId of 123 to my component/page using router and/or props it would look like this:
import { createNamespacedHelpers } from "vuex";
import projectModule from '#/store/project.module';
export default{
props['projectId'], // eg. 123
...
beforeCreate() {
// dynamic namespace built using whatever module you want:
let namespace = projectModule.buildNamespace(this.$options.propsData.projectId); // 'project:123'
// ... everything else as above with no need to drop namespaces everywhere
this.$options.computed = {
...mapState({
name: state => state.name,
description: state => state.description
})
}
}
}
Hope you find this useful.
I tackled this problem for hours, too. Then I finally came up with one idea.
Add attachStore function in a child vue component. A function nama is not important. Any name is ok except vue reserved word.
export default {
:
attachStore (namespace) {
Object.assign(this.computed, mapGetters(namespace, ['filters']))
}
}
When this vue component is imported, call attachStore with namespace parameter. Then use it at parent components attributes.
import Child from './path/to/child'
Child.attachStore('someStoresName')
export default {
name: 'parent',
components: { Child }
:
}
The error you're encountering is being thrown during Vue/Vuex's initialization process, this.store cannot be converted because it doesn't exist yet. I haven't had to work with namespacing yet, and this is untested so I don't know if it will work, but you may be able to solve this problem by having an intermediary like this:
<script>
export default {
props: ['store'],
data {
namespace: (this.store !== undefined) ? this.store : 'null',
},
computed: {
...mapGetters(this.namespace, [
'filters'
])
}
}
</script>
That ternary expression will return a string if this.store is undefined, if it isn't undefined then it will return the value in this.store.
Note that there is also a discussion about this on Vue's Github page here: https://github.com/vuejs/vuex/issues/863
Until Vue formally supports it, I replaced something like
...mapState({
foo: state => state.foo
})
with
foo () {
return this.$store.state[this.namespace + '/foo'] || 0
}
Where namespace is passed to my child component using a prop:
props: {
namespace: { type: String, required: true }
}
I'm new to vue, so I'm probably making a rookie error.
I have a root vue element - raptor.js:
const Component = {
el: '#app',
store,
data: {
productList: store.state.productlist
},
beforeCreate: function () {
return store.dispatch('getProductList', 'getTrendingBrands');
},
updated: function (){
console.log(111);
startSlider();
}
};
const vm = new Vue(Component);
Using this template
<div class="grid-module-single popular-products" id="app">
<div class="row">
<div class="popular-items-slick col-xs-12">
<div v-for="product in productList">
...
</div>
</div>
</div>
My store is very simple store/index.js:
import Vue from 'vue';
import Vuex from 'vuex';
import model from '../../utilities/model';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
productlist: []
},
mutations: {
setProductList(state, data) {
state.productlist = data;
}
},
actions: {
getProductList({ commit }, action) {
return model.products().then(data => commit('setProductList', data));
}
}
});
In my vuex devtool, I can see, that the store is being updated
https://www.screencast.com/t/UGbw7JyHS3
but my component is not being updated:
https://www.screencast.com/t/KhXQrePEd
Question:
I can see from the devtools, that my code is working. The store is being updated with data. My component is not being updated,however. I thought it was enough just to add this in the data property on the component:
data: {
productList: store.state.productlist
}
but apparently the data object doesn't seem to be automatically synced with the store. So either I'm doing a complete vue no-no somewhere, or I need to tweak the code a bit. Anyway can anyone help me in the right direction.
Thanks a lot.
UPDATE
Figured it out myself. Just had to replace the components data part with a computed method:
data:
data: {
productList: store.state.productlist
}
and replace it with.
computed: {
productList () {
return store.state.productlist;
}
},
data only work once on component before render, so you can use computed instead.
like above answer, or you can use mapstate
import {mapState} from 'vuex'
...
computed: mapState({
productList: state => state.productList
})
First - use getter to do this mapGetters, also you need to watch this property somehow, you can set store subscription or just with watch method trough component.
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'UPDATE_DATA') {
...
}
}
You are calling the store into the productList data property in the wrong way.
You can try it:
data: {
productList: $store.state.productlist
}
Otherwise you have to import store in each component that are using the store.