Vue md-input with Vuex updating only in one direction - javascript

I have a Vue app with an input element bound to a like this:
<template>
<input v-model="this.$store.state.myvalue"/>
</template>
and VueX store/index.js:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
myvalue: null
},
mutations: {},
actions: {},
modules: {}
});
When I modify myvalue with Vue devtools, the input's value changes too, but when I change the value in the input field, the state variable does not change. What am I doing wrong? I'm new to VueX.

Although it's not suggested to use vuex state directly bound with view layer, instead vuex is better to use for business logic, you can achieve changing the state on user input by below mentioned ways:
[1] two-way data binding: use v-model directive & bind the state in
it. On user input, the state will be updated. On changing state
programmatically, the element's value will be updated & reflect on
dom.
.vue file
<template>
<input v-model="$store.state.myvalue"/>
</template>
[2] manually create two-way data-binding.
.vue file
<template>
<input :value="getMyValue" #input="handleInput"/>
</template>
<script>
export default {
methods: {
handleInput (value) {
this.$store.commit('UPDATE_MY_VALUE', { value })
}
},
computed: {
getMyValue () {
return this.$store.state.myvalue
}
}
}
</script>
store file
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
myvalue: null
},
mutations: {
UPDATE_MY_VALUE (state, { value }) {
state.myvalue = value
}
},
actions: {},
modules: {}
});

when I change the value in the input field, the state variable does not change.
It does change, Dev tools just don't show the change. You can validate by changing the template to this:
<template>
<div>
<input type="text" v-model="$store.state.myvalue">
<div>{{ $store.state.myvalue }}</div>
</div>
</template>
But you should not mutate Vuex state like this! Vuex allows it but it's not recommended. Reason is your state changes should be traceable (easier to find which component changed the state and when). That's why Vuex recommends changing the state only by using mutations. Mutation is basically a function which is called when state change is needed.
Best way to do 2-way data binding against Vuex state is using computed properties with getter/seter like this:
<template>
<div>
<input v-model="myvalue">
<div>{{ myvalue }}</div>
</div>
</template>
<script>
export default {
name: "HelloWorld",
computed: {
myvalue: {
get: function() {
return this.$store.state.myvalue;
},
set: function(value) {
this.$store.commit("change_myvalue", value);
}
}
}
};
</script>
You need to define a mutation in your store to make it work like this:
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
myvalue: ""
},
mutations: {
change_myvalue(state, value) {
state.myvalue = value
}
},
actions: {},
modules: {}
});
You can read more about mutations here

Related

Reactive property is not propagating to component in Vue 3 app

I have a Vue 3 app. I am trying to setup a store for state management. In this app, I have the following files:
app.vue
component.vue
main.js
store.js
These files include the following:
store.js
import { reactive } from 'vue';
const myStore = reactive({
selectedItem: null
});
export default myStore;
main.js
import { createApp } from 'vue';
import App from './app.vue';
import myStore from './store';
const myApp = createApp(App);
myApp.config.globalProperties.$store = myStore;
myApp.mount('#app');
component.vue
<template>
<div>
<div v-if="item">You have selected an item</div>
<div v-else>Please select an item</div>
<button class="btn btn-primary" #click="generateItem">Generate Item</button>
</div>
</template>
<script>
export default {
props: {
item: Object
},
watch: {
item: function(newValue, oldValue) {
alert('The item was updated.');
}
},
methods: {
generateItem() {
const item = {
id:0,
name: 'Some random name'
};
this.$emit('itemSelected', item);
}
}
}
</script>
app.vue
<template>
<component :item="selectedItem" #item-selected="onItemSelected" />
</template>
<script>
import Component form './component.vue';
export default {
components: {
'component': Component
},
data() {
return {
...this.$store
}
},
methods: {
onItemSelected(item) {
console.log('onItemSelected: ');
console.log(item);
this.$store.selectedItem = item;
}
}
}
</script>
The idea is that the app manages state via a reactive object. The object is passed into the component via a property. The component can then update the value of the object when a user clicks the "Generate Item" button.
I can see that the selectedValue is successfully passed down as a property. I have confirmed this by manually setting selectedValue to a dummy value to test. I can also see that the onItemSelected event handler works as expected. This means that events are successfully flowing up. However, when the selectedItem is updated in the event handler, the updated value is not getting passed back down to the component.
What am I doing wrong?
$store.selectedItem stops being reactive here, because it's read once in data:
data() {
return {
...this.$store
}
}
In order for it to stay reactive, it should be either converted to a ref:
data() {
return {
selectedItem: toRef(this.$store, 'selectedItem')
}
}
Or be a computed:
computed: {
selectedItem() {
return this.$store.selectedItem
}
}

Vue mapState non reactive

I try to get the state from the store using the mapState function, But I can't use the generated code that returns the values into my template code ...
<template>
// Some code
<template v-if="!isLoggedIn">
// Some code
</template>
<template v-else>
// Some code
{{ currentUser.name }}
</template>
// Some code
</template>
<script>
import { mapState } from "vuex";
export default {
// Some code
computed: {
...mapState({auth : ['currentUser', 'isLoggedIn','customers']})
}
}
</script>
instead the following code work
<script>
import { mapState } from "vuex";
export default {
// Some code
computed: {
currentUser() {
return this.$store.state.auth.currentUser
},
isLoggedIn() {
return this.$store.state.auth.isLoggedIn
},
}
}
</script>
Warning message
[Vue warn]: Property or method "isLoggedIn" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property. See: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.
Thanks in advance
The right syntax to access non-root properties is the following (using arrow functions) :
computed: {
...mapState({
currentUser: state => state.auth.currentUser,
isLoggedIn: state => state.auth.isLoggedIn,
customers: state => state.auth.customers
})}
Check the documentation.
If you're trying to access values from a namespaced vuex module called auth, pass the name of the module as the first argument and the array of values to map as the second argument:
computed: {
...mapState('auth', ['currentUser', 'isLoggedIn','customers'])
}
You can mapState the module and then use, say this.auth.isLoggedin

How to set initial values for data from vuex?

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
}
})

vuex store doesn't update component

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.

How to design a store in Vuex to handle clicks in nested, custom components?

I'm trying to design a store to manage the events of my Vuex application. This far, I have the following.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = { dataRows: [], activeDataRow: {} };
const mutations = {
UPDATE_DATA(state, data) { state.dataRows = data; state.activeDataRow = {}; },
};
export default new Vuex.Store({ state, mutations });
I'm going to have a number of list items that are supposed to change the value of the data in the store when clicked. The design of the root component App and the menu bar Navigation is as follows (there will be a bunch of actions in the end so I've collected them in the file actions.js).
<template>
<div id="app">
<navigation></navigation>
</div>
</template>
<script>
import navigation from "./navigation.vue"
export default { components: { navigation } }
</script>
<template>
<div id="nav-bar">
<ul>
<li onclick="console.log('Clickaroo... ');">Plain JS</li>
<li #click="updateData">Action Vuex</li>
</ul>
</div>
</template>
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
}
}
</script>
Clicking on the first list item shows the output in the console. However, when clicking on the second one, there's nothing happening, so I'm pretty sure that the event isn't dispatched at all. I also see following error when the page's being rendered:
Property or method "updateData" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.
I'm very new to Vuex so I'm only speculating. Do I need to put in reference to the updateData action in the store, alongside with state and mutations? How do I do that? What/where's the "data option" that the error message talks about? Isn't it my components state and it's properties?
Why the error
You are getting the error, because when you have <li #click="updateData"> in the template, it looks for a method updateData in the vue component which it does not find, so it throws the error. To resolve this, you need to add corresponding methods in the vue component like following:
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
What this.$store.dispatch("updateData") is doing is calling your vuex actions as documented here.
What/where's the "data option"
You don't have any data properties defined, data properties for a vue component can be used, if you want to use that only in that component. If you have data which needs to be accessed across multiple components, you can use vuex state as I believe you are doing.
Following is the way to have data properties for a vue component:
<script>
import { updateData } from "../vuex_app/actions";
export default {
date: {
return {
data1 : 'data 1',
data2 : {
nesteddata: 'data 2'
}
}
}
vuex: {
getters: { activeDataRow: state => state.activeDataRow },
actions: { updateData }
},
methods:{
updateData: () => this.$store.dispatch("updateData")
}
}
</script>
You can use these data properties in the views, have computed properies based on it, or create watchers on it and many more.

Categories

Resources