Vuex variable properly updated but not refreshed in template - vuex

When I change the value of my v-text-field, I can see from the Vue tab of the developer tools that the value of lastName is updated in the user store with my code below. But, for some reason, the values of my 3 tests in my template below are not updated (or synchronized). Why ? What's wrong in my code ?
pages/home.vue:
<template>
<v-container>
<v-container>
<v-row>
<v-col>
<v-text-field v-model="lastName" />
</v-col>
</v-row>
</v-container>
TEST 1:
{{ $store.state.user.last_name }}
TEST 2:
{{ lastName }}
TEST 3:
<v-text-field v-model="lastName" />
</v-container>
</template>
<script>
export default {
computed: {
lastName: {
get() {
return this.$store.state.user.last_name
},
set(value) {
this.$store.commit('user/UPDATE_PROP', ['last_name', value])
}
}
}
}
</script>
store/user.js
export const mutations = {
UPDATE_PROP(state, payload) {
state[payload[0]] = payload[1]
}
}

I think the problem is in the way you set up your store. According to the Nuxt documentation,
you create a store directory and
every .js file inside the store directory is transformed as a namespaced module (index being the root module).
Also, according to the documentation,
... your state value should always be a function to avoid unwanted shared state on the server side.
Your state should look something like this
store/user.js
export const state = () => ({
last_name: ''
})
export const mutations = {
UPDATE_PROP(state, payload) {
state[payload[0]] = payload[1]
}
}

Related

Vuex mutate object best practices

I'm developing a module that enables users to place remarks on an object. I use a Vuex store and a component to visualize the remarks. I've pasted the code below.
Thing is I bind the text area that displays the remark body to a v-model that is the property body of the remark object. Since I return the array of remarks from the store with the getremarks getter any changes to the remark object properties are directly changed in the store as well since it is the same object that gets passed around. I thereby effectively bypass the entire action/mutation pattern Vuex wants me to implement.
I can always pass the mutated remark to an action and mutation in the store but that means I'm mutating an object that is allready mutated. I was wondering if there are any best practices around?
Store
import Vue from 'vue'
const model = {
state: {
remarks: [],
},
mutations: {
SET_REMARKS(state, remarks) {
state.remarks = remarks;
}
},
getters: {
getRemarks(state) {
return state.remarks;
},
getRemarkById: (state) => (id) => {
return state.remarks.find(remark => remark.model_remark_id === id);
}
},
actions: {
}
}
export default model;
Component
<template>
<v-list two-line expand>
<v-list-group v-for="remark in remarks" :key="remark.model_remark_id">
<template v-slot:activator>
<v-list-item-icon>
<v-icon v-text="icon" :color="remark.priority === 1 ? 'red' : remark.priority === 2 ? 'orange' : '#1478c7'" />
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title><b>{{ remark.created_by_user.name }}</b></v-list-item-title>
<v-list-item-subtitle>Created at {{ moment(remark.created_at).format('DD-MM-YYYY HH:ss:mm') }}</v-list-item-subtitle>
</v-list-item-content>
</template>
<v-list-item>
<v-list-item-icon>
</v-list-item-icon>
<v-list-item-content>
<v-row>
<v-col>
<v-textarea
class="mx-2"
v-model="remark.body"
auto-grow
/>
</v-col>
</v-row>
<v-row>
<v-col align="right">
<v-btn
color="primary"
elevation="2"
#click="remarkDone(remark)"
>Done</v-btn>
<v-btn
color="primary"
elevation="2"
#click="remarkUpdate(remark)"
>Update</v-btn>
</v-col>
</v-row>
</v-list-item-content>
</v-list-item>
</v-list-group>
</v-list>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
export default {
data: () => ({
icon: "mdi-comment"
}),
computed: {
remarks: function () {
return this.getRemarks();
},
},
methods: {
...mapGetters([
"getRemarks",
"getRemarkById"
]),
remarkUpdate(remark) {
console.log(remark);
const x = this.getRemarkById()(remark.model_remark_id);
console.log(x);
}
},
};
</script>
I think the key principle to keep in mind here is separation of concerns. Let Vuex handle all mutations of the state and let your Vue component simply fetch the remarks from the state. The state is reactive, so if you call a mutation from your component to change your remarks, you want the component to simply react to that change and render the updated markup.
Also, you should use mapGetters to map your getters to computed properties, not methods. I'm also not sure if you need getRemarkById getter in updateRemark() since you already have the remark as an argument.
export default {
computed: {
...mapGetters({ remarks: 'getRemarks' })
},
methods: {
updateRemark(remark) {
// here you should commit a mutation to update your remark
this.$store.commit('updateRemark', remark)
}
}
}

Limiting the two way binding with vuejs / variable values change when modifying another variable

I have a 'client-display' component containing a list of clients that I get in my store via mapGetter. I use 'v-for' over the list to display all of them in vuetify 'v-expansion-panels', thus one client = one panel. In the header of those panels, I have a 'edit-delete' component with the client passed to as a prop. This 'edit-delete' basically just emits 'edit' or 'delete' events when clicked on the corresponding icon with the client for payload. When I click on the edit icon, the edit event is then catched in my 'client-display' so I can assign the client to a variable called 'client' (sorry I know it's confusing a bit). I pass this variable to my dialog as a prop and I use this dialog to edit the client.
So the probleme is : When I edit a client, it does edit properly, but if I click on 'cancel', I find no way to revert what happened in the UI. I tried keeping an object with the old values and reset it on a cancel event, but no matter what happens, even the reference values that I try to keep in the object change, and this is what is the most surprising to me. I tried many things for this, such as initiating a new object and assigning the values manually or using Object.assign(). I tried a lot of different ways to 'unbind' all of this, nothing worked out. I'd like to be able to wait for the changes to be commited in the store before it's visible in the UI, or to be able to have a reference object to reset the values on a 'cancel' event.
Here are the relevant parts of the code (I stripped a lot of stuff to try and make it easier to read, but I think everything needed is there):
Client module for my store
I think this part works fine because I get the clients properly, though maybe something is binded and it should not
const state = {
clients: null,
};
const getters = {
[types.CLIENTS] : state => {
return state.clients;
},
};
const mutations = {
[types.MUTATE_LOAD]: (state, clients) => {
state.clients = clients;
},
};
const actions = {
[types.FETCH]: ({commit}) => {
clientsCollection.get()
.then((querySnapshot) => {
let clients = querySnapshot.docs.map(doc => doc.data());
commit(types.MUTATE_LOAD, clients)
}).catch((e) => {
//...
});
},
}
export default {
state,
getters,
mutations,
...
}
ClientsDisplay component
<template>
<div>
<div>
<v-expansion-panels>
<v-expansion-panel
v-for="c in clientsDisplayed"
:key="c.name"
>
<v-expansion-panel-header>
<div>
<h2>{{ c.name }}</h2>
<edit-delete
:element="c"
#edit="handleEdit"
#delete="handleDelete"
/>
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
//the client holder displays the client's info
<client-holder
:client="c"
/>
</v-expansion-panel-content>
</v-expansion-panel>
</v-expansion-panels>
</div>
<client-add-dialog
v-model="clientPopup"
:client="client"
#cancelEdit="handleCancel"
/>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import * as clientsTypes from '../../../../store/modules/clients/types';
import ClientDialog from './ClientDialog';
import EditDelete from '../../EditDelete';
import ClientHolder from './ClientHolder';
import icons from '../../../../constants/icons';
export default {
name: 'ClientsDisplay',
components: {
ClientHolder,
ClientAddDialog,
EditDelete,
},
data() {
return {
icons,
clientPopup: false,
selectedClient: null,
client: null,
vueInstance: this,
}
},
created() {
this.fetchClients();
},
methods: {
...mapGetters({
'stateClients': clientsTypes.CLIENTS,
}),
...mapActions({
//this loads my clients in my state for the first time if needed
'fetchClients': clientsTypes.FETCH,
}),
handleEdit(client) {
this.client = client;
this.clientPopup = true;
},
handleCancel(payload) {
//payload.uneditedClient, as defined in the dialog, has been applied the changes
},
},
computed: {
isMobile,
clientsDisplayed() {
return this.stateClients();
},
}
}
</script>
EditDelete component
<template>
<div>
<v-icon
#click.stop="$emit('edit', element)"
>edit</v-icon>
<v-icon
#click.stop="$emit('delete', element)"
>delete</v-icon>
</div>
</template>
<script>
export default {
name: 'EditDelete',
props: ['element']
}
</script>
ClientDialog component
Something to note here : the headerTitle stays the same, even though the client name changes.
<template>
<v-dialog
v-model="value"
>
<v-card>
<v-card-title
primary-title
>
{{ headerTitle }}
</v-card-title>
<v-form
ref="form"
>
<v-text-field
label="Client name"
v-model="clientName"
/>
<address-fields
v-model="clientAddress"
/>
</v-form>
<v-card-actions>
<v-btn
#click="handleCancel"
text
>Annuler</v-btn>
<v-btn
text
#click="submit"
>Save</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import AddressFields from '../../AddressFields';
export default {
name: 'ClientDialog',
props: ['value', 'client'],
components: {
AddressFields,
},
data() {
return {
colors,
clientName: '',
clientAddress: { province: 'QC', country: 'Canada' },
clientNote: '',
uneditedClient: {},
}
},
methods: {
closeDialog() {
this.$emit('input', false);
},
handleCancel() {
this.$emit('cancelEdit', { uneditedClient: this.uneditedClient, editedClient: this.client})
this.closeDialog();
},
},
computed: {
headerTitle() {
return this.client.name
}
},
watch: {
value: function(val) {
// I watch there so I can reset the client whenever I open de dialog
if(val) {
// Here I try to keep an object with the value of this.client before I edit it
// but it doesn't seem to work as I intend
Object.assign(this.uneditedClient, this.client);
this.clientName = this.client.name;
this.clientContacts = this.client.contacts;
this.clientAddress = this.client.address;
this.clientNote = '';
}
}
}
}
</script>
To keep an independent copy of the data, you'll want to perform a deep copy of the object using something like klona. Using Object.assign is a shallow copy and doesn't protect against reference value changes.

VueJS can't update v-text-field value dynamically

I dynamically draw form input component (as in the image) using this code:
In this case the key can be "name","gruppo","codice" and so on.
<v-row>
<v-col v-for="(key,i) in keys_visible" :key="key" v-if="headers_visible[i].visible == true" cols="12" sm="12" md="12"
v-if="!(headers_visible[i].type == 'bit' && editedItem[key] == -9)">
<v-text-field #change="comp_change(key)" v-else-if="headers_visible[i].type == 'varchar'" v-model="editedItem[key]" :label="headers_visible[i].text"></v-text-field>
</v-col>
</v-row>
Then I have comp_change function which is defined in methods block:
comp_change (par1) {
var self = this;
self.editedItem["name"] = "example text";
},
I have placed a debugger; at the beginning of comp_change function, and it stops everytime so the function is triggered, but without displaying new value in "Nome" field (which v-model is editedItem["name"]). Why after comp_change I can't see "example text" in the field?
The form is already opened when I fire change
This is likely a reactivity issue. You should read up on this here. Also, if you use v-model, you do not need to set the value yourself, meaning you can do away with the #change call. You have two options as I see it.
a. Use root data objects on your component instead of an array/object and then use v-model as normal. This looks like:
<template>
<v-text-field v-model="name" />
<v-text-field v-model="email" />
</template>
<script>
export default {
data() {
return {
name: '',
email: '',
etc: ''
}
}
}
</script>
Now, when your form fields are updated by the user, you won't need to use #change to set the value. It will happen automatically.
b. Or, set the model with Vue.set(). In this case, you are not going to use v-model. Instead, you have defined your own methods to manage the data. This looks like:
<template>
<v-text-field #change="comp_change(key)" />
</template>
<script>
import Vue from 'vue';
export default {
data() {
return {
editedItem: {}
}
},
methods: {
comp_change (par1) {
Vue.set( this.editedItem, 'name', 'example text' );
}
}
}
</script>

Passing Boolean prop results in Invalid prop: type check failed for prop "value"

I'm attempting to pass a boolean value from Vuex store as a prop into a component to control the visibility on an alert. I pass the prop to the alerts value property.
The result is the error:
[Vue warn]: Invalid prop: type check failed for prop "value". Expected Boolean, got String with value "globalAlert.alert".
I've adjusted how I pass the to the component a few different ways, removes parens, etc. But it appears to be taking my globalAlert as a literal String, even though I've validated that the Vuex getter is returning the globalAlert error properly.
Implementation
This store controls the global alert state
const state = {
globalAlert: {
alert: false,
alertTitle: "",
alertMessage: ""
}
}
const getters = {
getGlobalAlert: state => state.globalAlert
}
const actions = {
setGlobalAlert({ commit }, error) {
commit('setAlert', error)
}
}
const mutations = {
setAlert: (state, globalAlert) => (state.globalAlert = globalAlert)
}
export default {
state,
getters,
actions,
mutations
}
This is alert component with globalAlert prop
<template>
<div id="app">
<v-app id="inspire">
<div>
<v-alert
value="globalAlert.alert"
dismissible
color="red"
transition="slide-y-transition"
dark
dense
border="left"
elevation="2"
class="ma-5"
icon="error"
>
Something bad just happened
</v-alert>
<div class="text-center">
<v-btn v-if="!alert" dark #click="alert = true">Reset Alert</v-btn>
</div>
</div>
</v-app>
</div>
</template>
<script>
export default {
name: "GlobalAlert",
props: {
globalAlert: {}
}
};
</script>
This is the usage of the component, where globalAlert is pulled from the store and sent down to the component
...
<GlobalAlert
:globalAlert="globalAlert">
</GlobalAlert>
...
import { mapGetters, mapActions } from "vuex";
import GlobalAlert from "../components/GlobalAlert";
...
components: {GlobalAlert},
computed: {
...mapGetters(["isAuthenticated", "userDetails", "getGlobalError"]),
globalAlert(){
return this.$store.state.error.globalAlert;
}
}

Cannot read property 'length' of undefined"

I am getting the error below. The weird part is that I'm positive the data is there because in my vue add on I can see that it successfully grabs the information from the vuex store. My initial guess is that somehow the data is not yet grabbed from the store, at the point that it creates the template?
Vue warn]: Error in render: "TypeError: Cannot read property 'length' of undefined"
The data: 'spaces' is grabbed from the store.
export default {
name: "myspaces",
data() {
return {
filterMaxLength: 3,
selectedSpace: 0,
selectedRoom: 0
}
},
created() {
// Default selected space (first in json)
this.selectedSpace = this.spaces[0].id;
// Default selected room (first in json)
this.selectedRoom = this.spaces[0].rooms[0].id;
},
computed: {
// Get 'spaces' from store.
...mapState([
'spaces'
])
}
Template:
<template>
<div>
<v-flex v-if="spaces.length < filterMaxLength">
<v-btn v-for="space in spaces">
<h4> {{space.name}} </h4>
</v-btn>
</v-flex>
</div>
<template>
The store:
import Vuex from 'vuex'
Vue.use(Vuex);
export default new Vuex.Store({
state: {
spaces:[
{
id:1,
name:'House in Amsterdam',
rooms:[
{
id:1,
name:'Bedroom Otto',
},
{
id:2,
name:'Bedroom Mischa'
}
]
},
{
id:2,
name:'Office in Amsterdam',
rooms:[
{
id:1,
name:'Office 1',
},
{
id:2,
name:'Office 2'
}
]
}
]} });
The vue chrome add on says this information is in the component:
Always before check length, be sure your property its set then check length
<v-flex v-if="spaces && spaces.length < filterMaxLength">
Update ECMAScript 2020
You can use Optional chaining for this purpose too
<v-flex v-if="spaces?.length < filterMaxLength">
You should use Object.keys(spaces).length, such as:
<template>
<div>
<v-flex v-if="typeof spaces !== 'undefined' && typeof spaces === 'object' && Object.keys(spaces).length < filterMaxLength">
<v-btn v-for="space in spaces">
<h4> {{space.name}} </h4>
</v-btn>
</v-flex>
</div>
<template>
just to be sure you have following in your vue
import { mapState } from "vuex";
Else, you can use getters also, eg. :
In your vue file
v-if="this.getSpaces.length !== 0"
In computed fonctions of your vue file
getSpaces() {
return this.$store.getters.getSpaces;
}
In your store
getters: {
getSpaces: state => {
return state.spaces;
}
},
I also had this problem, I imported a sass style sheet with functions(#mixins) and a sass style sheet with variables into one sass style sheet.
I imported them as
(
#import url('./_variables.scss');
#import url('./_func.scss');
)
so i just took out the url() for both import statements and that error went away.