My first Vue project and I want to run a loading effect on every router call.
I made a Loading component:
<template>
<b-loading :is-full-page="isFullPage" :active.sync="isLoading" :can-cancel="true"></b-loading>
</template>
<script>
export default {
data() {
return {
isLoading: false,
isFullPage: true
}
},
methods: {
openLoading() {
this.isLoading = true
setTimeout(() => {
this.isLoading = false
}, 10 * 1000)
}
}
}
</script>
And I wanted to place inside the router like this:
router.beforeEach((to, from, next) => {
if (to.name) {
Loading.openLoading()
}
next()
}
But I got this error:
TypeError: "_components_includes_Loading__WEBPACK_IMPORTED_MODULE_9__.default.openLoading is not a function"
What should I do?
Vuex is a good point. But for simplicity you can watch $route in your component, and show your loader when the $route changed, like this:
...
watch: {
'$route'() {
this.openLoading()
},
},
...
I think it's fast and short solution.
I don't think you can access a component method inside a navigation guard (beforeEach) i would suggest using Vuex which is a vue plugin for data management and then making isLoading a global variable so before each route navigation you would do the same ... here is how you can do it :
Of course you need to install Vuex first with npm i vuex ... after that :
on your main file where you are initializing your Vue instance :
import Vue from 'vue'
import Vuex from 'vue'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
isLoading: false,
},
mutations: {
openLoading(state) {
state.isLoading = true
setTimeout(() => {
state.isLoading = false
}, 10000)
},
},
})
// if your router is on a separated file just export the store and import it there
const router = new VueRouter({
routes: [
{
// ...
},
],
})
router.beforeEach((to, from, next) => {
if (to.name) {
store.commit('openLoading')
}
next()
})
new Vue({
/// ....
router,
store,
})
In your component:
<b-loading :is-full-page="isFullPage" :active.sync="$store.state.isLoading" :can-cancel="true"></b-loading>
Related
In vuejs 2 it's possible to assign components to global variables on the main app instance like this...
const app = new Vue({});
Vue.use({
install(Vue) {
Vue.prototype.$counter = new Vue({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ },
}
});
}
})
app.$mount('#app');
But when I convert that to vue3 I can't access any of the properties or methods...
const app = Vue.createApp({});
app.use({
install(app) {
app.config.globalProperties.$counter = Vue.createApp({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ }
}
});
}
})
app.mount('#app');
Here is an example for vue2... https://jsfiddle.net/Lg49anzh/
And here is the vue3 version... https://jsfiddle.net/Lathvj29/
So I'm wondering if and how this is still possible in vue3 or do i need to refactor all my plugins?
I tried to keep the example as simple as possible to illustrate the problem but if you need more information just let me know.
Vue.createApp() creates an application instance, which is separate from the root component of the application.
A quick fix is to mount the application instance to get the root component:
import { createApp } from 'vue';
app.config.globalProperties.$counter = createApp({
data: () => ({ value: 1 }),
methods: {
increment() { this.value++ }
}
}).mount(document.createElement('div')); 👈
demo 1
However, a more idiomatic and simpler solution is to use a ref:
import { ref } from 'vue';
const counter = ref(1);
app.config.globalProperties.$counter = {
value: counter,
increment() { counter.value++ }
};
demo 2
Not an exact answer to the question but related. Here is a simple way of sharing global vars between components.
In my main app file I added the variable $navigationProps to global scrope:
let app=createApp(App)
app.config.globalProperties.$navigationProps = {mobileMenuClosed: false, closeIconHidden:false };
app.use(router)
app.mount('#app')
Then in any component where I needed that $navigationProps to work with 2 way binding:
<script>
import { defineComponent, getCurrentInstance } from "vue";
export default defineComponent({
data: () => ({
navigationProps:
getCurrentInstance().appContext.config.globalProperties.$navigationProps,
}),
methods: {
toggleMobileMenu(event) {
this.navigationProps.mobileMenuClosed =
!this.navigationProps.mobileMenuClosed;
},
hideMobileMenu(event) {
this.navigationProps.mobileMenuClosed = true;
},
},
Worked like a charm for me.
The above technique worked for me to make global components (with only one instance in the root component). For example, components like Loaders or Alerts are good examples.
Loader.vue
...
mounted() {
const currentInstance = getCurrentInstance();
if (currentInstance) {
currentInstance.appContext.config.globalProperties.$loader = this;
}
},
...
AlertMessage.vue
...
mounted() {
const currentInstance = getCurrentInstance();
if (currentInstance) {
currentInstance.appContext.config.globalProperties.$alert = this;
}
},
...
So, in the root component of your app, you have to instance your global components, as shown:
App.vue
<template>
<v-app id="allPageView">
<router-view name="allPageView" v-slot="{Component}">
<transition :name="$router.currentRoute.name">
<component :is="Component"/>
</transition>
</router-view>
<alert-message/> //here
<loader/> //here
</v-app>
</template>
<script lang="ts">
import AlertMessage from './components/Utilities/Alerts/AlertMessage.vue';
import Loader from './components/Utilities/Loaders/Loader.vue';
export default {
name: 'App',
components: { AlertMessage, Loader }
};
</script>
Finally, in this way you can your component in whatever other components, for example:
Login.vue
...
async login() {
if (await this.isFormValid(this.$refs.loginObserver as FormContext)) {
this.$loader.activate('Logging in. . .');
Meteor.loginWithPassword(this.user.userOrEmail, this.user.password, (err: Meteor.Error | any) => {
this.$loader.deactivate();
if (err) {
console.error('Error in login: ', err);
if (err.error === '403') {
this.$alert.showAlertFull('mdi-close-circle', 'warning', err.reason,
'', 5000, 'center', 'bottom');
} else {
this.$alert.showAlertFull('mdi-close-circle', 'error', 'Incorrect credentials');
}
this.authError(err.error);
this.error = true;
} else {
this.successLogin();
}
});
...
In this way, you can avoid importing those components in every component.
I use vuex, and in my page module I set title and content, I arranged a destroyed method to reset them, If I click a different component there is no problem while resetting values but when I click a different static page, component is not destroyed and data is not updated. Is there a way to reset vuex state values for the same component.?
const state = {
page: {},
};
const getters = {
page(state) {
return state.page;
},
};
const mutations = {
setAPage (state, pPage) {
state.page = pPage
state.errors = {}
},
setCleanPage(state){
state.page = null
},
reset(state) {
const s = state();
Object.keys(s).forEach(key => {
state[key] = s[key];
});
console.log('state', state)
}
}
const actions = {
fetchAPage (context, payload) {
context.commit("setCleanPage");
const {slug} = payload;
return ApiService.get(`pages/${slug}/`)
.then((data) => {
context.commit("setAPage", data.data);
})
.catch((response) => {
context.commit("setError", response.data)
})
},
resetAPage(context){
context.commit("reset");
}
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
and in my component :
<script>
import { mapGetters, mapActions, mapMutations } from "vuex";
export default {
name: "Page",
computed: {
...mapGetters('pages', {page: 'page'}),
},
beforeRouteLeave (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params /foo/:id, when we
// navigate between /foo/1 and /foo/2, the same Foo component instance
// will be reused, and this hook will be called when that happens.
// has access to >this component instance.
console.log(to)
console.log(from)
console.log(next)
this.$store.dispatch('pages/resetAPage');
},
methods: {
...mapActions(['pages/fetchAPage']),
},
destroyed() {
this.toggleBodyClass('removeClass', 'landing-page');
this.$store.dispatch('pages/resetAPage');
},
created() {
this.$store.dispatch('pages/fetchAPage' , this.$route.params)
},
};
</script>
How can I reset or update data for the same component ?
Thanks
You can use this package for your resets - https://github.com/ianwalter/vue-component-reset
You can use a beforeRouteLeave guard in your component(s) when you want to catch the navigation away from the route where the component is being used (https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards)
The beforeRouteUpdate component guard is called only when the component was used in the current route and is going to be reused in the next route.
I would suggest you watch for that param change.
I don't know how you make use of your params, but you can pass them to your component as props and then add a watcher on them which would call your vuex reset action.
// in your router
// ... some routes
{
path: "/page/:id",
props: true, // this passes :id as prop to your component
component: Page
}
In you component
export default {
name: "Page",
props: ["id"], // your route param
computed: {
...mapGetters('pages', {page: 'page'}),
},
beforeRouteLeave (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params /foo/:id, when we
// navigate between /foo/1 and /foo/2, the same Foo component instance
// will be reused, and this hook will be called when that happens.
// has access to >this component instance.
console.log(to)
console.log(from)
console.log(next)
this.$store.dispatch('pages/resetAPage');
},
methods: {
...mapActions(['pages/fetchAPage']),
},
watch: {
id() { // watch the id and reset the page if it has changed or add additionnal logic as needed
this.$store.dispatch('pages/resetAPage');
}
},
destroyed() {
this.toggleBodyClass('removeClass', 'landing-page');
this.$store.dispatch('pages/resetAPage');
},
created() {
this.$store.dispatch('pages/fetchAPage' , this.$route.params)
},
};
Context: I am trying to get place data via the place_id on the beforeEnter() route guard. Essentially, I want the data to load when someone enters the url exactly www.example.com/place/{place_id}. Currently, everything works directly when I use my autocomplete input and then enter the route but it does not work when I directly access the url from a fresh tab. I believe the issue is because google has not been created yet.
Question: How can I access PlacesService() using the beforeEnter() route guard ?
Error: Uncaught (in promise) ReferenceError: google is not defined
Example Code:
In one of my store modules:
const module = {
state: {
selectedPlace: {}
},
actions: {
fetchPlace ({ commit }, params) {
return new Promise((resolve) => {
let request = {
placeId: params,
fields: ['name', 'rating', 'formatted_phone_number', 'geometry', 'place_id', 'website', 'review', 'user_ratings_total', 'photo', 'vicinity', 'price_level']
}
let service = new google.maps.places.PlacesService(document.createElement('div'))
service.getDetails(request, function (place, status) {
if (status === 'OK') {
commit('SET_SELECTION', place)
resolve()
}
})
})
},
},
mutations: {
SET_SELECTION: (state, selection) => {
state.selectedPlace = selection
}
}
}
export default module
In my store.js:
import Vue from 'vue'
import Vuex from 'vuex'
import placeModule from './modules/place-module'
import * as VueGoogleMaps from 'vue2-google-maps'
Vue.use(Vuex)
// gmaps
Vue.use(VueGoogleMaps, {
load: {
key: process.env.VUE_APP_GMAP_KEY,
libraries: 'geometry,drawing,places'
}
})
export default new Vuex.Store({
modules: {
placeModule: placeModule
}
})
in my router:
import store from '../state/store'
export default [
{
path: '/',
name: 'Home',
components: {
default: () => import('#/components/Home/HomeDefault.vue')
}
},
{
path: '/place/:id',
name: 'PlaceProfile',
components: {
default: () => import('#/components/PlaceProfile/PlaceProfileDefault.vue')
},
beforeEnter (to, from, next) {
store.dispatch('fetchPlace', to.params.id).then(() => {
if (store.state.placeModule.selectedPlace === undefined) {
next({ name: 'NotFound' })
} else {
next()
}
})
}
}
]
What I've tried:
- Changing new google.maps.places.PlacesService to new window.new google.maps.places.PlacesService
- Using beforeRouteEnter() rather than beforeEnter() as the navigation guard
- Changing google.maps... to gmapApi.google.maps... and gmapApi.maps...
- Screaming into the abyss
- Questioning every decision I've ever made
EDIT: I've also tried the this.$gmapApiPromiseLazy() proposed in the wiki here
The plugin adds a mixin providing this.$gmapApiPromiseLazy to Vue instances (components) only but you're in luck... it also adds the same method to Vue statically
Source code
Vue.mixin({
created () {
this.$gmapApiPromiseLazy = gmapApiPromiseLazy
}
})
Vue.$gmapApiPromiseLazy = gmapApiPromiseLazy
So all you need to do in your store or router is use
import Vue from 'vue'
// snip...
Vue.$gmapApiPromiseLazy().then(() => {
let service = new google.maps.places....
})
I have a Vue component that uses Vuex with namespaced modules.
// Main Component
import store from './store';
import {mapState, mapGetters, mapActions} from 'vuex';
new Vue({
store,
computed: {
...mapState({
showModal: state => state.showModal,
}),
computed: {
...mapState('ModuleA', {
a: state => state.a,
}),
...mapState('ModuleB', {
b: state => state.b,
}),
...mapGetters('ModuleA', { getA: 'getA' } ),
...mapGetters('ModuleB', { getB: 'getB' } ),
},
methods: {
...mapActions('ModuleA', [ 'doSomeA']),
...mapActions('ModuleB', [ 'doSomeB']),
},
mounted() {
let payLoad = { ... },
this.doSomeA(payload); // I never see this getting dispatched
}
// Store
export default new Vuex.Store({
modules: { ModuleA, ModuleB }
...
}
// Module A
export default {
namespaced: true,
actions: {
doSomeA: ({dispatch, commit, getters, rootGetters}) => payload => { // do something with payload }
...
}
Since I have mapped the action from my namespaced module I am simply calling the action like normal method in my component. But somehow its not dispatching the action. Any idea what is going wrong here?
How to bind functions in methods object. I believe if I use arrow function, it should auto bind with current object. However, it has its own scrope. Therefore, I cannot update data variables after http get request.
This is my customers component.
import axios from 'axios';
export default {
data () {
return {
customers: 'temp ',
loading: 'false',
error: null,
}
},
created () {
console.log(this)//this is fine
this.getCustomerList()
},
watch: {
'$route': 'getCustomerList'
},
methods: {
getCustomerList: () => {
console.log(this)
axios.get('/api/customers')
.then((res)=>{
if(res.status === 200){
}
})
}
}
}
This is result of console.log(this)..
This is my app.js file
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
import Customers from './components/Customers/Customers.vue'
const router = new VueRouter({
mode: 'history',
base: __dirname,
history: true,
routes: [
{ path: '/customers', component: Customers }
]
})
new Vue ({
router
}).$mount('#app')
Try following:
methods: {
getCustomerList () {
console.log(this)
var that = this
axios.get('/api/customers')
.then((res)=>{
if(res.status === 200){
//use that here instead of this
}
})
}
}