How to call bootstrap-vue modals and toasts from vuex actions? - vue.js

Did anyone tried to use boostrap-vue in combination with vuex?
I'm having hard time calling modals and toast from vuex actions.
Obviously I can not use this from vuex store, therefore I can't use:
this.$bvModal.show('modalId');
I also tried calling modal like this
import Vue from 'vue';
Vue.prototype.$bvModal.show('transaction');
But console gives me following warning:
BootstrapVue warn]: '$bvModal' must be accessed from a Vue instance 'this' context
Any idea how I can call modals and toasts from vuex actions directly?

Try to call this._vm.$bvModal.show('modalId');.
Reference.

I think a better approach is to prevent to deal with the UI from the Store. So, you can add a store property and watch for changes from your components.
In the following example, I added an array toastMessages in the state property and a ADD_TOAST_MESSAGE mutation to add some toastMessage. You can then commit an ADD_TOAST_MESSAGE mutation from another mutation or from an action.
Inside your Top-level component (App.vue), you can watch for your toastMessages state property changes and display the last item that was pushed.
App.vue
<script>
export default {
name: "App",
created() {
this.$store.watch(
state => state.toastMessages,
toastMessages => {
this.$bvToast.toast(this.toastMessages.slice(-1)[0]);
}
);
}
}
</script>
store.js
export default new Vuex.Store({
state: {
toastMessages: []
},
mutations: {
ADD_TOAST_MESSAGE: (state, toastMessage) => (state.toastMessages = [...state.toastMessages, toastMessage]),
},
actions: {
myActionThatDoSomething({commit}, params) {
// Do something
commit('ADD_TOAST_MESSAGE', "Something happened");
}
}
});

found a solution here: https://github.com/vuejs/vuex/issues/1399#issuecomment-491553564
import App from './App.vue';
const myStore = new Vuex.Store({
state: {
...
},
actions: {
myAction(ctx, data) {
// here you can use this.$app to access to your vue application
this.$app.$root.$bvToast.toast("toast context", {
title: "toast!"
});
}
}
});
const app = new Vue({
el: '#my-div',
render: h => h(App),
store: myStore
});
myStore.$app = app; // <--- !!! this line adds $app to your store object

Using #eroak idea, I have implemented this same thing for vue-sweetalert2.
I have also created a store for my Sweet Alert Toaster, then I am watching a property called ticks that is updated whenever state of the toaster is updated.
I am using ticks as I only have one message in my state, and ticks just timestamp when action was triggered.
You can find whole demo here: https://github.com/Uraharadono/CallbackFromVuexAction

Try to call this._vm.$root.$bvModal.show('modalId');

Related

How do I commit a vuex store mutation from inside a vue-router route that imports "store"?

My goal is to commit (invoke/call) a mutation that I've defined in my Vuex store.
store/store.js
export default {
modules: {
app: {
state: {
shouldDoThing: false,
}
mutations: {
setShouldDoThing: (state, doThing) => { state.shouldDoThing = doThing },
}
}
}
}
Since I attach Vuex to my app, I can use this.$store.commit throughout the app in various components without issue.
main.js
import Store from 'store/store.js';
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const app = new Vue({
el: '#app-root',
store,
// ...etc
});
For example:
exampleComponent.vue
export default {
created() {
// This works!
this.$store.commit('setShouldDoThing', true);
},
}
Now I'd like to commit something from a vue-router Route file, in a beforeEnter method:
exampleRoute.js
import Store from 'store/store.js'
const someRoute = {
path: '/blah',
beforeEnter(to, from, next) {
Store.commit('setShouldDoThing', true);
next();
}
}
However, when I try the above, I get the error
TypeError: _state_store__WEBPACK_IMPORTED_MODULE_10__.default.commit is not a function
There's lots of examples online of successfully using vuex getters by importing. And, if I console.log() the Store import, I can see my entire store structure
modules:
app:
actions: {someAction: ƒ, …}
getters: {isStartingQuery: ƒ}
mutations: {ariaAnnounce: ƒ, …}
state: {…}
__proto__: Object
How can I import my Store and then commit a mutation from within a vue-router file?
I've been googling for a very long time, and didn't find a stackoverflow answer or a vue forums answer for this specific case or issue, so below is the solution that I tested and works in my case.
For whatever reason, I can't trigger commit. However, I can simply invoke the mutation directly, and this change is then reflected throughout other components (as in, a "different" store wasn't imported).
someRoute.js
import Store from 'store/store.js'
const someRoute = {
path: '/blah',
beforeEnter(to, from, next) {
Store.modules.app.mutations.setShouldDoThing(Store.modules.app.state, true);
next();
}
}
And later, in some component:
someComponent.vue
export default {
beforeMount() {
console.log(this.$store.state.app.shouldDoThing);
// true
}
}

How to pass data to component after action dispatched to Vuex?

I'm using Vuex to try manage application-level data. I fetch the data, using axios, then propogate that data to data variables in a component. Nothing complicated.
My store looks like this
// store.js
// appropriate imports and such
export const store = new Vuex.Store({
state: {
var: []
},
actions:{
getData(context){
axios.get('/endpoint').then(function(response){
context.state.var = res.data.value1;
console.log("action in store.js run");
//appropriate brackets,etc below
I then dispatch this action in my app.js
//appropriate imports and such above
const app = new Vue({
el: '#app',
store: store,
created() {
this.$store.dispatch('getRightSidebar')
console.log("action dispatched")
}
});
I use the created lifecycle hook to ensure this action gets dispatched before the component is mounted. Surely, at this point two messages should be logged to the console. One message from the created lifestyle hook and another from the actual action being dispatched. However, when I run the application only the former message is logged. Surely, when the action is dispacthed, the actual method/request me be called/executed.
Now, when I print the values of the state variables, from within the mounted lifecycle hook of the component, they're undefined. However, if I print the state it logs the object with the appropriate data
///component.js
mounted() {
console.log("component mounted")
console.log(this.$store.state.var) // undefined
console.log(this.$store.state) // Obeject with the appropriate data
}
So on one hand it seems to dispatch the action, but when I try to access individual objects from the state, it craps itself. Is there something wrong with the way I'm trying to access objects within the state?
you need to "wait" for getData promise to resolve
when created() hook runs there can be no data
export const store = new Vuex.Store({
state: {
var: []
},
actions:{
getRightSidebar(context){
// must return Promise to subscribe to it!
return axios.get('/endpoint').then(function(response){
context.state.var = res.data.value1;
console.log("action in store.js run");
const app = new Vue({
el: '#app',
store: store,
data: {
isLoading: true
},
async mounted() {
await this.$store.dispatch('getRightSidebar')
// after this there is gonna be some data
this.isLoading = false
}
})
<template>
<div>
<div v-if='isLoading'>loading...</div>
<div v-else>{{$store.state.yourvariable}}</div>
</div>
</template>

Fetch data from endpoint using vuex action and set store variables in vuex?

I need to get data from an endpoint, and then assign the data to state variables in the store.
The code looks something like this.
import Vue from 'vue'
import axios from 'axios'
import Vuex from 'vuex'
Vue.use(Vuex)
export const store = new Vuex.Store({
state: {
sample: 'foo',
sample2 : [],
sample3: []
},
getters:{
},
actions:{
getData(context){
axios.get('/endpoint').then(function(response){
console.log(context.state);
console.log(context.state.sample);
console.log(response);
context.state.sample = 'bar';
console.log(context.state.sample);
context.state.sample2 = response.data.sample2data;
context.state.sample3 = response.data.sample3data;
}
);
}
},
mutations:{
}
});
The trouble is the application doesn't execute the axios request at all, as far as I can tell. I've tested the endpoint elsewhere and I'm certain there's nothing wrong with the request itself. Surely, everytime my application is mounted the request should be executed?
In the mounted method of your App.js you can do
mounted() {
this.$store.dispatch('getData')
}
You need to dispatch the action in the created method, where you create your vue instance. If the action is dispatched in a component, the store is only injected after the component is mounted. The data will only load after mounting the component which means component(s) that need to read the data, will have access to it, however the data in the state will be undefined on load.
export const store = new Vuex.Store({
state: {
sample2 : [], // undefined
sample3: [] // undefined
},
getters:{
},
actions:{
getData(context){
axios.get('/endpoint').then(function(response){
context.state.sample2 = response.data.sample2data;
context.state.sample3 = response.data.sample3data;
}
);
}
},
mutations:{
}
});
The below ensures that action is dispatched before mounting any components, and therefore the components will be able to read the state after the action has properly set the values
import {store} from 'path/to/store/';
new Vue({
el: '#app',
computed:,
store: store,
created() {
this.$store.dispatch('getData') // dispatch loading
}
})

Vuetify v-select not showing items after full refresh

I have a dialog with a v-select that doesn't show values in the drop down after a full page load. It does work after a hot module reload triggered by modifying the component file.
Using vuetify:
"vuetify": "^1.5.17",
Component contains:
template code:
<v-select
:items="routes"
label="Routes"
multiple
chips
persistent-hint
hint="Send to which routes"
v-model="message.routes"
></v-select>
routes is a computed property:
routes: {
get() {
return this.$store.state.routes
}
}
The data gets downloaded in the created event:
created() {
this.downloadRoutes()
}
This maps to a store method that does an AJAX call that commits the returned list:
downloadRoutes({ commit, state }) {
return new Promise((resolve, reject) => {
commit('SET_LOADING', true)
api.get('/route').then(response => {
var routes = response.data.routes
commit('SET_ROUTES', routes)
commit('SET_LOADING', false)
resolve()
})
.catch(function(error) {
commit('SET_LOADING', false)
reject()
})
})
},
AJAX response is just an array of routes:
{"routes":["Route1","Route2","RouteXX"]}
This I have shown by doing a console.log of the response and the state in the computed routes property.
What am I missing?
It sounds like your problem lies somewhere inside the vue instance lifecycle, i.e. you might call this.downloadRoutes() inside the wrong hook. Do your problem still occure if you try with a hardcoded array of Routes?
Found the problem.
My store is initialised via an initialState function.
export default new Vuex.Store({
state: initialState,
getters: {
......
The initial state function declares all the top level collections used in state.
function initialState() {
return {
fullName: localStorage.fullName,
routes: [] // Was missing
}
Adding routes in that initial state solves the problem.
I think it's because watchers on state are not added when new attributes are added to state.
When I do a full page refresh the routes gets added to state after the select is initialized and so it is not watching for changes. During hot module reload the routes is already in the state and is so picked up by the select.

How to use Vue Router from Vuex state?

In my components I've been using:
this.$router.push({ name: 'home', params: { id: this.searchText }});
To change route. I've now moved a method into my Vuex actions, and of course this.$router no longer works. Nor does Vue.router. So, how do I call router methods from the Vuex state, please?
I'm assuming vuex-router-sync won't help here as you need the router instance.
Therefore although this doesn't feel ideal you could set the instance as a global within webpack, i.e.
global.router = new VueRouter({
routes
})
const app = new Vue({
router
...
now you should be able to: router.push({ name: 'home', params: { id: 1234 }}) from anywhere within your app
As an alternative if you don't like the idea of the above you could return a Promise from your action. Then if the action completes successfully I assume it calls a mutation or something and you can resolve the Promise. However if it fails and whatever condition the redirect needs is hit you reject the Promise.
This way you can move the routers redirect into a component that simply catches the rejected Promise and fires the vue-router push, i.e.
# vuex
actions: {
foo: ({ commit }, payload) =>
new Promise((resolve, reject) => {
if (payload.title) {
commit('updateTitle', payload.title)
resolve()
} else {
reject()
}
})
# component
methods: {
updateFoo () {
this.$store.dispatch('foo', {})
.then(response => { // success })
.catch(response => {
// fail
this.$router.push({ name: 'home', params: { id: 1234 }})
})
I a situation, I find myself to use .go instead of .push.
Sorry, no explanation about why, but in my case it worked. I leave this for future Googlers like me.
I believe rootState.router will be available in your actions, assuming you passed router as an option in your main Vue constructor.
As GuyC mentioned, I was also thinking you may be better off returning a promise from your action and routing after it resolves. In simple terms: dispatch(YOUR_ACTION).then(router.push()).
state: {
anyObj: {}, // Just filler
_router: null // place holder for router ref
},
mutations: {
/***
* All stores that have this mutation will run it
*
* You can call this in App mount, eg...
* mounted () {
* let vm = this
* vm.$store.commit('setRouter', vm.$router)
* }
*
setRouter (state, routerRef) {
state._router = routerRef
}
},
actions: {
/***
* You can then use the router like this
* ---
someAction ({ state }) {
if (state._router) {
state._router.push('/somewhere_else')
} else {
console.log('You forgot to set the router silly')
}
}
}
}
Update
After I published this answer I noticed that defining it the way I presented Typescript stopped detecting fields of state. I assume that's because I used any as a type. I probably could manually define the type, but it sounds like repeating yourself to me. That's way for now I ended up with a function instead of extending a class (I would be glad for letting me know some other solution if someone knows it).
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default (router: VueRouter) => {
return new Store({
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
import Vue from 'vue'
import App from './App.vue'
import createStore from './store'
// ...
new Vue({
router,
store: createStore(router),
render: createElement => createElement(App)
}).$mount('#app')
Initial answer content
I personally just made a wrapper for a typical Store class.
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default class extends Store<any> {
constructor (router: VueRouter) {
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
}
If You need $route You can just use router.currentRoute. Just remember You rather need router reactive if You want Your getters with router.currentRoute to work as expected.
And in "main.ts" (or ".js") I just use it with new Store(router).
import Vue from 'vue'
import App from './App.vue'
import Store from './store'
// ...
new Vue({
router,
store: new Store(router),
render: createElement => createElement(App)
}).$mount('#app')