Using Vuex Store in Modal resets the store after closing of modal - vuex

I'm using Nuxt 2.11 with "bootstrap-vue" 2.5.0
I created a store with a simple list and a mutator to add an element to this list.
// store/issues.js
export const state = () => ({
list: [],
})
export const mutations = {
add(state, issue) {
state.list.push(issue)
},
}
Then I created a page with a for-loop over all entries and a computed property to load the data from the store.
computed: {
issues() {
return this.$store.state.issues.list
},
},
If I add a simple button to explicit add an item, it will be added everytime and everythings works fine
<b-btn #click="test">Add</b-btn>
test() {
this.$store.commit('issues/add', {
title: 'title',
})
},
But when I use a bootstrap-vue-form to submit new items to the store, the item will be added at first, but after a second, the whole store is removed and the list on my page is empty.
<b-form #submit="onSubmit" #reset="onReset">
...
</b-form>
onSubmit() {
this.$store.commit('issues/add', this.issue)
},

If you do not want the page to reload on submit of the form, you need to prevent the native form submit from occuring:
<b-form #submit="onSubmit" #reset="onReset">
...
</b-form>
onSubmit(evt) {
evt.preventDefault()
this.$store.commit('issues/add', this.issue)
},

Related

Cypress spy not being called when VueJS component emits event

I'm trying to follow the guide here to test an emitted event.
Given the following Vue SFC:
<script setup>
</script>
<template>
<button data-testid="credits" #click="$emit('onCredits')">Click</button>
</template>
and the following Cypress test:
import { createTestingPinia } from '#pinia/testing';
import Button from './Button.vue';
describe('<Button />', () => {
it('renders', () => {
const pinia = createTestingPinia({
createSpy: cy.spy(),
});
cy.mount(Button, {
props: {
onCredits: cy.spy().as('onCreditsSpy'),
},
global: {
plugins: [pinia],
},
});
cy.get('[data-testid=credits]').click();
cy.get('#onCreditsSpy').should('have.been.called');
});
});
My test is failing with
expected onCreditsSpy to have been called at least once, but it was never called
It feels weird passing in the spy as a prop, have I misunderstood something?
I solved such a situation with the last example within Using Vue Test Utils.
In my case the PagerElement component uses the properties 'pages' for the total of pages to render and 'page' for the current page additional to the 'handleClick'-Event emitted once a page has been clicked:
cy.mount(PagerElement, {
props: {
pages: 5,
page: 0
}
}).get('#vue')
Within the test I click on the third link that then emmits the Event:
cy.get('.pages router-link:nth-of-type(3)').click()
cy.get('#vue').should(wrapper => {
expect(wrapper.emitted('handleClick')).to.have.length
expect(wrapper.emitted('handleClick')[0][0]).to.equal('3')
})
First expectation was for handleClick to be emitted at all, the second one then checks the Parameters emitted (In my case the Page of the element clicked)
In order to have the Wrapper-element returned a custom mount-command has to be added instead of the default in your component.ts/component.js:
Cypress.Commands.add('mount', (...args) => {
return mount(...args).then(({ wrapper }) => {
return cy.wrap(wrapper).as('vue')
})
})

Force props to update in child component after Vuex update

I have a child component (<BaseProjectTable>) that is re-used throughout my site that contains a Vuetify <v-data-table> component. The headers and content items for the data table are passed into the the component as props, and the item data is mapped into the parent as a mapGetter from a Vuex store. The child component contains editing functionality for each row, and I'm using a mapAction to update the API and Vuex data from there, with the idea being that since my mapGetter is reactive, it should update the data and hence the data table display. However, this is not working; I can see via dev tools that state is updated just fine, but the child component display is not.
Here is the relevant portion of the child <BaseProjectTable> component:
<template>
<div>
<v-data-table
show-expand
:headers="headers"
:items="filteredItems"
:search="search"
tem-key="sow"
#click:row="rowClick"
:hide-default-footer="disablePagination"
dense
:disable-pagination="disablePagination"
>
...
</v-data-table>
...
export default {
name: "BaseProjectTable",
props: {
headers: Array,
items: Array,
loggedInUser: Object,
title: String,
max2chars: v => v.length <= 2 || 'Input too long!',
editDialog: false,
showPracticeFilter: {
default: true,
type: Boolean
},
showSEFilter: {
default: true,
type: Boolean
},
showStatusFilter: {
default: true,
type: Boolean
},
projectType: {
type: String,
default: 'active'
},
disablePagination: {
type: Boolean,
default: true
},
},
},
methods: {
...mapActions('projects', {
saveProject: 'saveProject',
}),
save() {
// Update API and store with new data.
this.saveProject({
projectType: this.projectType,
projectData: this.editedItem
})
},
computed: {
statuses() {
return this.projectType === 'active' ? this.activeStatuses : this.oppStatuses;
},
filteredItems() {
return this.items.filter(d => {
return Object.keys(this.filters).every(f => {
return this.filters[f].length < 1 || this.filters[f].includes(d[f])
})
})
},
}
and here is the relevant code from the parent component:
<v-card>
<BaseProjectTable
:headers="headers"
:items="activeProjects"
:loggedInUser="loggedInUser"
title="Active Projects"
:disablePagination=false
></BaseProjectTable>
</v-card>
...
export default {
computed: {
...mapGetters('projects', {
activeProjects: 'activeProjects',
closedProjects: 'closedProjects',
opportunities: 'opportunities'
}),
}
}
The save() method updates the data in my Vuex store that is referenced by the activeProjects map getter (I have verified in the Vue devtools that this is true). It also shows the items value in the component itself updated in the Components tab in the dev tools. Since using mapGetters makes the data reactive, I expected that it would also update the data in my child component, but it doesn't.
Based on what I saw here, I tried the item-key property of the <v-data-table> like so:
<v-data-table
show-expand
:headers="headers"
:items="filteredItems"
:search="search"
item-key="sow"
#click:row="rowClick"
:hide-default-footer="disablePagination"
dense
:disable-pagination="disablePagination"
>
(every record using this component will have the unique sow key), but that didn't work.
The only think I could think if is how I'm editing the data in my mutation:
export const state = () => ({
active: [],
closed: [],
opportunities: [],
})
export const getters = {
activeProjects: state => state.active,
closedProjects: state => state.closed,
opportunities: state => state.opportunities,
}
export const actions = {
saveProject({ commit }, {projectType, projectData}) {
commit(types.SAVE_PROJECT, {projectType, projectData});
}
}
export const mutations = {
[types.SAVE_PROJECT](state, {projectType, projectData}) {
// Get project from state list by sow field.
const index = state[projectType].findIndex(p => p.sow === projectData.sow);
state[projectType][index] = projectData;
}
}
as compared to replacing the entire state[projectType] value.
What do I need to do to get the data table to display my updated value?
From the Vue documentation,
Vue cannot detect the following changes to an array
When you directly set an item with the index, e.g. vm.items[indexOfItem] = newValue
When you modify the length of the array, e.g. vm.items.length = newLength
Replace what you have with
import Vue from 'vue'; // this should be at the top level
export const mutations = {
[types.SAVE_PROJECT](state, {projectType, projectData}) {
// Get project from state list by sow field.
const index = state[projectType].findIndex(p => p.sow === projectData.sow);
Vue.set(state[projectType], index, projectData)
}
}
After this, the changes to the array will be detected and the getter will work as expected.

How to open vuetify dialog after user logs in to application

In my application I want to show a modal to introduce the user in my application, so it will appear only in the first time he logs in. What I am doing is storing isNewUser in the global state and using it to know if it should render the modal or not using the same process described in this answer. (I'm not using event bus)
Here is my parent component:
<template>
<Intro :value="isNewUser" #input="finishTutorial" />
</template>
mounted() {
const store = this.$store;
this.isNewUser = store.state.auth.user.isNewUser;
},
When the user logs in and this component is rendered I saw the dialog being rendered and closing. If I hit f5 it reloads the page and dialog is showed correctly.
If I do the bellow modification it works, but I don't want to solve the problem this way since it won't work for all cases, it will depend on the speed of the user computer/internet.
mounted() {
setTimeout(() => {
const store = this.$store;
this.isNewUser = store.state.auth.user.isNewUser;
}, 2000);
},
I've tried using v-if as well
<template>
<Intro v-if="isNewUser" :value="true" #input="finishTutorial" />
</template>
<script>
export default {
components: {
Intro,
},
data() {
return {
isNewUser: false,
};
},
mounted() {
const store = this.$store;
this.isNewUser = store.state.auth.user.isNewUser;
},
methods: {
async finishTutorial() {
this.$store.dispatch('auth/finishTutorial');
this.isNewUser = false;
},
},
};
</script>
You can use a computed property to do so:
computed: {
isNewUser() {
return this.$store.state.auth.user.isNewUser;
}
}
and in the template you would do like so:
<template>
<Intro :value="isNewUser" #input="finishTutorial" />
</template>

Vue 2 triggering method on child component

I will be happy if i can either trigger and event or call a method within the vue-button component when the user object gets updated. I need to make sure that this only happens on this specific vue-button and not on any other ones on the page.
The button controls a state object that styles the button depending on response from the server. Will display red if an error response is returned or green if a successful response is returned. In ether case the button then is also disabled so users can't spam click it. I need to have the ability to reset the button from the parent when the user is updated.
I do not believe that a global event bus is the solution because i need granular control over which components respond to the event and this button is used in a lot of places.
<template>
<div class="row">
<div class="col-12">
<basic-input-field :resource="user" :set-resource="setUserProperty" property="name" label="Name"></basic-input-field>
<basic-input-field :resource="user" :set-resource="setUserProperty" property="email" label="Email"></basic-input-field>
<basic-input-field :resource="user" :set-resource="setUserProperty" property="password" label="Password"></basic-input-field>
<vue-button :on-click="updateUser" label="Save"></vue-button>
</div>
</div>
</template>
<script>
import axios from 'axios';
import basicInputField from '../general/forms/basic-input-field.vue';
import vueButton from '../general/buttons/vue-button.vue';
export default {
name: 'user',
data() {
return {
};
},
mixins: [],
components: {
basicInputField,
vueButton
},
computed: {
user() {
return this.$store.state.user;
},
},
mounted() {
this.$httpGet('user', {id: 5});
},
watch: {
'user': function (newUser) {
// I want to trigger an event inside the component vue-button
// I do not want to trigger then event in every vue-button component on the page just this vue-button
// I need to call a resetStatus method within the vue-button component when the user changes
// This would have worked in vue 1 with an event 'resetStatus' that would call the method in the vue-button component
this.$broadcast('resetStatus');
}
},
methods: {
setUserProperty(property, value) {
this.$store.commit('UPDATE_MODULE_RESOURCE', {module: 'user', resource: property, value: value});
},
updateUser() {
return this.$httpPut('user', {id: this.user.id}, this.user);
}
},
};
</script>

Vuex accessing state BEFORE async action is complete

I'm having issues where a computed getter accesses the state before it is updated, thus rendering an old state. I've already tried a few things such as merging mutations with actions and changing state to many different values but the getter is still being called before the dispatch is finished.
Problem
State is accessed before async action (api call) is complete.
Code structure
Component A loads API data.
User clicks 1 of the data.
Component A dispatches clicked data (object) to component B.
Component B loads object received.
Note
The DOM renders fine. This is a CONSOLE ERROR. Vue is always watching for DOM changes and re-renders instantly. The console however picks up everything.
Goal
Prevent component B (which is only called AFTER component) from running its computed getter method before dispatch of component A is complete.
Store.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios';
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
searchResult: {},
selected: null,
},
getters: {
searchResult: state => {
return state.searchResult;
},
selected: state => {
return state.selected;
},
},
mutations:{
search: (state, payload) => {
state.searchResult = payload;
},
selected: (state, payload) => {
state.selected = payload;
},
},
actions: {
search: ({commit}) => {
axios.get('http://api.tvmaze.com/search/shows?q=batman')
.then(response => {
commit('search', response.data);
}, error => {
console.log(error);
});
},
selected: ({commit}, payload) => {
commit('selected', payload);
},
},
});
SearchResult.vue
<template>
<div>
//looped
<router-link to="ShowDetails" #click.native="selected(Object)">
<p>{{Object}}</p>
</router-link>
</div>
</template>
<script>
export default {
methods: {
selected(show){
this.$store.dispatch('selected', show);
},
},
}
</script>
ShowDetails.vue
<template>
<div>
<p>{{Object.name}}</p>
<p>{{Object.genres}}</p>
</div>
</template>
<script>
export default {
computed:{
show(){
return this.$store.getters.selected;
},
},
}
</script>
This image shows that the computed method "show" in file 'ShowDetails' runs before the state is updated (which happens BEFORE the "show" computed method. Then, once it is updated, you can see the 2nd console "TEST" which is now actually populated with an object, a few ms after the first console "TEST".
Question
Vuex is all about state watching and management so how can I prevent this console error?
Thanks in advance.
store.dispatch can handle Promise returned by the triggered action handler and it also returns Promise. See Composing Actions.
You can setup your selected action to return a promise like this:
selected: ({commit}, payload) => {
return new Promise((resolve, reject) => {
commit('selected', payload);
});
}
Then in your SearchResults.vue instead of using a router-link use a button and perform programmatic navigation in the success callback of your selected action's promise like this:
<template>
<div>
//looped
<button #click.native="selected(Object)">
<p>{{Object}}</p>
</button>
</div>
</template>
<script>
export default {
methods: {
selected(show){
this.$store.dispatch('selected', show)
.then(() => {
this.$router.push('ShowDetails');
});
},
},
}
</script>
You can try to use v-if to avoid rendering template if it is no search results
v-if="$store.getters.searchResult"
Initialize your states.
As with all other Vue' data it is always better to initialize it at the start point, even with empty '' or [] but VueJS (not sure if Angular or React act the same, but I suppose similar) will behave much better having ALL OF YOUR VARIABLES initialized.
You can define initial empty value of your states in your store instance.
You will find that helpful not only here, but e.g. with forms validation as most of plugins will work ok with initialized data, but will not work properly with non-initialized data.
Hope it helps.