vue - how to pass callback to vuex action - javascript

I am figuring how to pass callback to vuex action
I tried below code but not working. The code run before I fire it
src/store/modules/web3.module.js
import Web3 from "web3";
const state = {};
const getters = {};
const mutations = {};
const actions = {
async test(context, confirmCallback, rejectCallback) {
confirmCallback();
rejectCallback();
}
};
export default {
state,
getters,
actions,
mutations
};
App.vue
<template>
<div id="app"></div>
</template>
<script>
import { mapActions } from "vuex";
export default {
name: "App",
methods: {
...mapActions(["test"]),
onModalOpen() {
console.log("open");
},
onModalClose() {
console.log("Close");
},
},
async created() {
let result = await this.test({
confirmCallback: this.onModalOpen(),
rejectCallback: this.onModalClose(),
});
},
};
</script>

The issue takes place in 2 places:
the payload syntax in your store is wrong
you are firing the functions with the () at the end when passing it through an object:
Solve the payload issue
An action has 2 parameters, first comes the context which is an object that contains the state, mutations etc. then comes the payload which is an object aswell
const actions = {
async test(context, {confirmCallback, rejectCallback}) {
confirmCallback();
rejectCallback();
}
}
Solve the decleration issue
To solve the decleration issue simply remove the () at the end like shown below:
async created() {
let result = await this.test({
confirmCallback: this.onModalOpen,
rejectCallback: this.onModalClose,
});
},

Related

jest mock vuex useStore() with vue 3 composition api

I'm trying to unit test a component where you click a button which should then call store.dispatch('favoritesState/deleteFavorite').
This action then calls an api and does it's thing. I don't want to test the implementation of the vuex store, just that the vuex action is called when you click the button in the component.
The Component looks like this
<template>
<ion-item :id="favorite.key">
<ion-thumbnail class="clickable-item remove-favorite-item" #click="removeFavorite()" slot="end" id="favorite-star-thumbnail">
</ion-thumbnail>
</ion-item>
</template>
import {useStore} from "#/store";
export default defineComponent({
setup(props) {
const store = useStore();
function removeFavorite() {
store.dispatch("favoritesState/deleteFavorite", props.item.id);
}
return {
removeFavorite,
}
}
});
The jest test
import {store} from "#/store";
test(`${index}) Test remove favorite for : ${mockItemObj.kind}`, async () => {
const wrapper = mount(FavoriteItem, {
propsData: {
favorite: mockItemObj
},
global: {
plugins: [store]
}
});
const spyDispatch = jest.spyOn(store, 'dispatch').mockImplementation();
await wrapper.find('.remove-favorite-item').trigger('click');
expect(spyDispatch).toHaveBeenCalledTimes(1);
});
I have tried different solutions with the same outcome. Whenever the "trigger('click')" is run it throws this error:
Cannot read properties of undefined (reading 'dispatch') TypeError:
Cannot read properties of undefined (reading 'dispatch')
The project is written in vue3 with typescript using composition API and vuex4
I found a solution to my problem.
This is the solution I ended up with.
favorite.spec.ts
import {key} from '#/store';
let storeMock: any;
beforeEach(async () => {
storeMock = createStore({});
});
test(`Should remove favorite`, async () => {
const wrapper = mount(Component, {
propsData: {
item: mockItemObj
},
global: {
plugins: [[storeMock, key]],
}
});
const spyDispatch = jest.spyOn(storeMock, 'dispatch').mockImplementation();
await wrapper.find('.remove-favorite-item').trigger('click');
expect(spyDispatch).toHaveBeenCalledTimes(1);
expect(spyDispatch).toHaveBeenCalledWith("favoritesState/deleteFavorite", favoriteId);
});
This is the Component method:
setup(props) {
const store = useStore();
function removeFavorite() {
store.dispatch("favoritesState/deleteFavorite", favoriteId);
}
return {
removeFavorite
}
}

How to properly install a Pinia Store?

I'm building a Vue 3 app using the OptionsAPI along with a Pinia Store but I frequently run into an issue stating that I'm trying to access the store before createPinia() is called.
I've been following the documentation to use the Pinia store outside components as well, but maybe I'm not doing something the proper way.
Situation is as follows:
I have a login screen (/login) where I have a Cognito session manager, I click a link, go through Cognito's signup process, and then get redirected to a home route (/), in this route I also have a subroute that shows a Dashboard component where I make an API call.
On the Home component I call the store using useMainStore() and then update the state with information that came on the URL once I got redirected from Cognito, and then I want to use some of the state information in the API calls inside Dashboard.
This is my Home component, which works fine by itself, due to having const store = useMainStore(); inside the mounted() hook which I imagine is always called after the Pinia instance is created.
<template>
<div class="home">
<router-view></router-view>
</div>
</template>
<script>
import {useMainStore} from '../store/index'
export default {
name: 'Home',
components: {
},
mounted() {
const store = useMainStore();
const paramValues = {}
const payload = {
// I construct an object with the properties I need from paramValues
}
store.updateTokens(payload); // I save the values in the store
},
}
</script>
Now this is my Dashboard component:
<script>
import axios from 'axios'
import {useMainStore} from '../store/index'
const store = useMainStore();
export default {
name: "Dashboard",
data() {
return {
user_data: null,
}
},
mounted() {
axios({
url: 'myAPIUrl',
headers: { 'Authorization': `${store.token_type} ${store.access_token}`}
}).then(response => {
this.user_data = response.data;
}).catch(error => {
console.log(error);
})
},
}
</script>
The above component will fail, and throw an error stating that I'm trying to access the store before the instance is created, I can solve this just by moving the store declaration inside the mounted() hook as before, but what if I want to use the store in other ways inside the component and not just in the mounted hook? And also, why is this failing? By this point, since the Home component already had access to the store, shouldn't the Dashboard component, which is inside a child route inside Home have the store instance already created?
This is my main.js file where I call the createPinia() method.
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const pinia = createPinia();
createApp(App).use(router).use(pinia).mount('#app')
And the error I get is:
Uncaught Error: [🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?
My Store file:
import { defineStore } from 'pinia';
export const useMainStore = defineStore('main', {
state: () => ({
access_token: sessionStorage.getItem('access_token') || '',
id_token: sessionStorage.getItem('id_token') || '',
token_type: sessionStorage.getItem('token_type') || '',
isAuthenticated: sessionStorage.getItem('isAuthenticated') || false,
userData: JSON.parse(sessionStorage.getItem('userData')) || undefined
}),
actions: {
updateTokens(payload) {
this.id_token = payload.id_token;
this.access_token = payload.access_token;
this.token_type = payload.token_type
sessionStorage.setItem('id_token', payload.id_token);
sessionStorage.setItem('access_token', payload.access_token);
sessionStorage.setItem('token_type', payload.token_type);
sessionStorage.setItem('isAuthenticated', payload.isAuthenticated);
},
setUserData(payload) {
this.userData = payload;
sessionStorage.setItem('userData', JSON.stringify(payload));
},
resetState() {
this.$reset();
}
},
})
It's possible but not common and not always allowed to use use composition functions outside a component. A function can rely on component instance or a specific order of execution, and current problem can happen when it's not respected.
It's necessary to create Pinia instance before it can be used. const store = useMainStore() is evaluated when Dashboard.vue is imported, which always happen before createPinia().
In case of options API it can be assigned as a part of component instance (Vue 3 only):
data() {
return { store: useMainStore() }
},
Or exposed as global property (Vue 3 only):
const pinia = createPinia();
const app = createApp(App).use(router).use(pinia);
app.config.globalProperties.mainStore = useMainStore();
app.mount('#app');
Since you're using Vue 3, I suggest you to use the new script setup syntax:
<script setup>
import { reactive, onMounted } from 'vue'
import axios from 'axios'
import { useMainStore } from '../store'
const store = useMainStore();
const data = reactive({
user_data: null
})
onMounted (async () => {
try {
const {data: MyResponse} = await axios({
method: "YOUR METHOD",
url: 'myAPIUrl',
headers: { 'Authorization': `${store.token_type} ${store.access_token}`}
})
data.user_data = MyResponse
} catch(error){
console.log(error)
}
})
</script>
Using setup you can define that store variable and use it through your code.
everyone after a lot of research I found the answer to this issue,
you must pass index.ts/js for const like below:
<script lang="ts" setup>
import store from '../stores/index';
import { useCounterStore } from '../stores/counter';
const counterStore = useCounterStore(store());
counterStore.increment();
console.log(counterStore.count);
</script>

Make shared property reactive in Vue Composition API composable by declaring variable outside of exported function

I am using the composition api plugin for vue2 (https://github.com/vuejs/composition-api) to reuse composables in my app.
I have two components that reuse my modalTrigger.js composable, where I'd like to declare some sort of shared state (instead of using a bloated vuex state management).
So in my components I do something like:
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'SearchButton',
setup(props, context) {
const { getModalOpenState, setModalOpenState } = modalTrigger();
return {
getModalOpenState,
setModalOpenState,
};
},
};
And in my modalTrigger I have code like:
import { computed, ref, onMounted } from '#vue/composition-api';
let modalOpen = false; // needs to be outside to be accessed from multiple components
export default function () {
modalOpen = ref(false);
const getModalOpenState = computed(() => modalOpen.value);
const setModalOpenState = (state) => {
console.log('changing state from: ', modalOpen.value, ' to: ', state);
modalOpen.value = state;
};
onMounted(() => {
console.log('init trigger');
});
return {
getModalOpenState,
setModalOpenState,
};
}
This works, but only because I declare the modalOpen variable outside of the function.
If I use this:
export default function () {
const modalOpen = ref(false); // <------
const getModalOpenState = computed(() => modalOpen.value);
...
It is not reactive because the modalTrigger is instantiated twice, both with it's own reactive property.
I don't know if that is really the way to go, it seems, that I am doing something wrong.
I also tried declaring the ref outside:
const modalOpen = ref(false);
export default function () {
const getModalOpenState = computed(() => modalOpen.value);
But this would throw an error:
Uncaught Error: [vue-composition-api] must call Vue.use(plugin) before using any function.
So what would be the correct way to achieve this?
I somehow expected Vue to be aware of the existing modalTrigger instance and handling duplicate variable creation itself...
Well, anyway, thanks a lot in advance for any hints and tipps.
Cheers
Edit:
The complete header.vue file:
<template>
<header ref="rootElement" :class="rootClasses">
<button #click="setModalOpenState(true)">SET TRUE</button>
<slot />
</header>
</template>
<script>
import { onMounted, computed } from '#vue/composition-api';
import subNavigation from '../../../../composables/subNavigation';
import mobileNavigation from '../../../../composables/mobileNavigation';
import search from '../../../../composables/searchButton';
import { stickyNavigation } from '../../../../composables/stickyNav';
import metaNavigation from '../../../../composables/metaNavigation';
import modalTrigger from '../../../../composables/modalTrigger';
export default {
name: 'Header',
setup(props, context) {
const { rootElement, rootClasses } = stickyNavigation(props, context);
mobileNavigation();
subNavigation();
search();
metaNavigation();
const { getModalOpenState, setModalOpenState } = modalTrigger();
onMounted(() => {
console.log('Header: getModalOpenState: ', getModalOpenState.value);
setModalOpenState(true);
console.log('Header: getModalOpenStat: ', getModalOpenState.value);
});
return {
rootClasses,
rootElement,
getModalOpenState,
setModalOpenState,
};
},
};
</script>
The composition API is setup somewhere else where there are Vue components mounted a bit differently than you normally would.
So I can't really share the whole code,but it has this inside:
import Vue from 'vue';
import CompositionApi from '#vue/composition-api';
Vue.use(CompositionApi)
The composition API and every other composable works just fine...

Load state with async action Vuex

I can't realize how to get it works.
I'm trying to load a propertie (productos) on my data() which has to catch the value from a state.
My component:
data () {
return {
productos: this.$store.state.mStock.productos
}
},
created() {
this.$store.dispatch('fetchProductos')
}
At this point i think it's okay, when i load my component i dispatch my action to load the state on the store.
I think the problem is that the way i fill the state is ASYNC
Store:
import StockService from '#/services/StockService'
export const moduleStock = {
strict: false,
state: {
productos: []
},
mutations: {
setProductos (state, payload) {
state.productos = payload.productos
}
},
actions: {
async fetchProductos ({commit}, payload) {
const resp = await (StockService.getProductos())
var productos = resp.data
commit('setProductos', {productos: productos})
}
}
}
When i load my component, the propertie "productos" on the data() is null, however if i see the 'state.productos' from the Vuex devtools, it has the data!
I'm messed up.
The data() method is only run once.
This might seem to work if when the component and the vue store use the same object instance, but doesn't work in this case because a new array instance is assigned in the store while the component stil has the previous instance (the empty array)
Use computed properties. I recommend using the mapState() helper:
computed: mapState({
productos: state => state.mStock.productos
})
without mapState you'd write:
computed: {
productos() {
return this.$store.state.mStock.productos
}
}

How do I export a string to another Vue file?

I have a masterData.js file that is a store for my master data, in short the file reads my mongo db data & sends it to other project components. I created a function to export the string in the masterData.js file as:
/ ***************************** MUTATIONS
const mutations = {
exportColumns (payload) {
Object.keys(payload[0]).map(x => { return x; });
}
}
Where payload will store all the rows and payload[0] holds the value of column header names. The output of this chunk of code is like this:
["_id","businessAreaName","businessAreaDisplay","councilDisplay","councilID"]
I want to transfer the values to masterData.vue file. My code on masterData.Vue is:
importColumns ()
{
let payload = {
vm: this,
mutation: 'masterData/exportColumns'
};
}
What else should I add to to check whether the column names are received or not?
If you're trying to access the data in your store from within a component, then you'll want to either just map the state to the component or map a getter to the component. Mutations are used by components (or by actions) to modify the state of the store. So instead you would do something like...
//masterData.js
//assuming this gets rolled up as a module called masterdata to the primary store
//store for payload state
const state = {
payload: null,
}
//allows payload to be set -- not sure how you are retrieving the payload but you can use this to store it however you get it
const mutations = {
setPayload (state, payload) {
state.payload = payload
}
}
//get just the columns
const getters = {
getColumns (state) {
Object.keys(state.payload[0]).map(x => { return x; })
}
}
export default {
state,
mutations,
getters,
}
Then
//masterData.vue
<template>
//...
</template>
<script>
import { mapGetters, mapState } from 'vuex'
export default {
computed: {
//I believe getting state from a store module requires a function like this
...mapState({
payload: function(state) {
return state.masterdata.payload
},
}),
//I think for getters you can just reference the method and it will find it
...mapGetters([
'getColumns',
])
},
}
</script>
This is how you import stuff in a single file component.
<template>
<!-- html stuff -->
</template>
<script>
import Mutations from 'yourModule.js'
export default {
name: 'YourComponent',
props: {},
data(){
return {
foo: 'foo'
}
},
methods{
mymethod() {
Mutations.exportColumn(this.foo);
},
}
}
</script>

Categories

Resources