let fetchData = async function() {
return $.get("https://jsonplaceholder.typicode.com/todos/1");
}
Vue.use(VueRouter);
const cComponent = {
data() {
return {
fetchedData: null
}
},
template: `<div>{{$route.params.id+': '}}{{fetchedData}}</div>`,
async beforeRouteEnter(to, from, next) {
let data = await fetchData();
next(vm => vm.fetchedData = data);
},
async beforeRouteUpdate(to, from, next) {
console.log("beforeRouteUpdate");
let data = await fetchData();
this.fetchedData = data;
next();
}
}
const routes = [{
path: "/path/:id",
component: cComponent,
}]
const router = new VueRouter({
mode: "history",
routes,
})
const app = new Vue({
el: "#app",
router,
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.14/dist/vue.js"></script>
<script src=" https://unpkg.com/vue-router#3.5.1/dist/vue-router.js"></script>
<div id="app">
<router-link to="/path/1">An URL</router-link>
<router-link to="/path/2">Another one</router-link>
<router-link to="/path/3">And another one</router-link>
<router-view :key="$route.path"></router-view>
</div>
This is the fetch function that retrieves data from a server:
let fetchPage = async (route) => {
return $.get('route');
}
This is my beforeRouteUpdate navigation guard, which fetchedData property of the Vue instance (declared in data option beforehand) does not changes.
async beforeRouteUpdate(to, from, next) {
this.fetchedData = await fetchPage(to.path);
//fetchData is not changed while changing route param
next();
}
The next() callback is activated at the end (URL does change), but this.fetchedData shows null in Vue DevTools (initializing value).
On the other hand, if I do the same thing with async/await in beforeRouteEnter hook, everything works perfectly.
async beforeRouteEnter(to, from, next) {
let data = await fetchPage(to.path);
next(app => app.fetchedData = data); //fetchedData has fetched data as supposed.
},
I tried replacing this with app just like in beforeRouteEnter hook. But there's still no result.
What am I missing? Is this the best practice with guard hooks and how should I use them better? (ignore Exception handling part)
Updated: SPA experience for code snippet on StackOverflow is kinda not perfect so you better try using my provided JS (here). It has the same problem but I simplified it.
Versions
Vue 2.6
VueRouter 3.5
Related
I'm using the useFetch to fetch the data in the composition api and then calling the function in onMounted hook in components, here is the code.
useShows.ts (composable)
export function useShows(){
var shows = useState<Show[]>('shows')
const fetchShows = async() => {
const {data, pending} = await useFetch<Show[]>('http://localhost:3000/shows')
shows.value = data.value
}
return {shows, fetchShows}
}
shows.vue
<script setup lang="ts">
var { shows, fetchShows } = useShows()
onMounted(() => {
console.log("On mounted called")
fetchShows()
})
</script>
<template>
<div>{{shows}}</div>
</template>
When I'm navigating to /shows from the home page it is working fine, but when I direct open the link localhost/shows it is not working and only giving me the null.
I have the exact same problem. I solved it by wrapping it within a nextTick.
await nextTick(async () => {
await fetchShows()
})
I do have the following code in my main.js file:
import Vue from 'vue';
import App from './components/App';
import router from './router';
const app = new Vue({
data: { loading: false },
router,
render: h => h(App),
}).$mount('#app');
Now i try to set the loading var from my router (./router/index.js):
router.beforeEach((to, from, next) => {
this.$root.loading = true;
next();
})
router.afterEach((to, from) => {
this.$root.loading = false;
next();
})
but it doesn't work. I always get
Cannot read property '$root' of undefined
Any ideas what I'm doing wrong?
You should use this in next callback because the guard is called before the navigation is confirmed, thus the new entering component has not even been created yet.
router.beforeEach((to, from, next) => {
//vm refers to this
next((vm)=>{vm.$root.loading = true;});
})
router.afterEach((to, from, next) => {
next((vm)=>{vm.$root.loading = false;});
})
I need to call below method in created(). For this purpose, I need to make created() as async. Per Vue documentation, created() is called synchronously. Will Vue Framework await on created() to avoid any race conditions?
this.isAuthenticated = await authService.isAuthenticated();
Vue.config.productionTip = false;
function tm(ms, msg) {
return new Promise(resolve => {
setTimeout(() => {
resolve(msg);
}, ms);
});
}
new Vue({
async beforeCreate() {
console.log(await tm(1000, "BEFORE CREATE"));
},
async created() {
console.log(await tm(2000, "CREATED"));
},
async beforeMount() {
console.log(await tm(3000, "BEFORE MOUNT"));
},
async mounted() {
console.log(await tm(4000, "MOUNTED"));
}
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
If you really need to wait until your asynchronous function is done. You can basically await it before you create the Vue instance. This may not be always useable, but in a case like in this question it is, and it is more solid than putting an asynchronous lifecycle hook that isn't awaited.
Vue.config.productionTip = false;
// mock service
const authService = {
isAuthenticated: () => new Promise((r) => setTimeout(() => r(true), 2000))
};
// async iife to await the promise before creating the instance
(async() => {
const isAuthenticated = await authService.isAuthenticated();
new Vue({
data: {
isAuthenticated
},
created() {
console.log('authenticaed:', this.isAuthenticated);
},
})
})().catch(console.warn);
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
I implemented a route guard to protect the /settings route with the vue-router method beforeEnter().
I try to test that the route is protected to admins only.
I am using Vuejs 2, Vue-router, Vuex and vue-test-utils.
router.js
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
export default new Router({
routes: [
..., // other routes
{
path: '/settings',
name: 'Settings',
component: () => import('./views/settings'),
beforeEnter: (to, from, next) => {
next(store.state.isAdmin);
}
}
]
});
the unit test:
test('navigates to /settings view if the user is admin', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueRouter);
const router = new VueRouter();
const wrapper = shallowMount(App, {
stubs: ['router-link', 'router-view'],
localVue,
mocks: {
$store: store
},
router
});
wrapper.vm.$route.push({ path: '/settings' });
// test if route is set correctly
});
current logs output:
wrapper.vm.$route` is undefined.
How can I mount the App correctly and access the router? How can I test the current route to verify that the admin user has been redirected succesfully?
Thank logan for the link. It seems like the best possible solution:
As of now there is no easy way to test navigation guards. If you want to simulate the event triggering by calling router.push function, you are going to have a hard time. A better easier solution is to call the guard manually in beforeEach(), but even this solution doesn't have a clean approach. See the following example:
beforeRouteEnter
// my-view.js
class MyView extends Vue {
beforeRouteEnter (to, from, next) {
next(function (vm) {
vm.entered = true;
});
}
}
// my-view.spec.js
it('should trigger beforeRouteEnter event', function () {
const view = mount(MyView);
const spy = sinon.spy(view.vm.$options.beforeRouteEnter, '0'); // you can't just call view.vm.beforeRouteEnter(). The function exists only in $options object.
const from = {}; // mock 'from' route
const to = {}; // mock 'to' route
view.vm.$options.beforeRouteEnter[0](to, from, cb => cb(view.vm));
expect(view.vm.entered).to.be.true;
expect(spy).to.have.been.called;
});
I'm trying to unit test (with vue-test-utils) a component, which has a beforeRouteUpdate in component navigation Guard like this:
<template>
...
</template>
<script>
export default {
computed: {
...
}
...
beforeRouteUpdate(to, from, next) {
// code to be tested
this.$store.dispatch('setActiveTask')
}
}
</script>
I do render the component in the test file with shallowMount and mock stuff like the $store.
beforeEach(() => {
cmp = shallowMount(Task, {
mocks: {
$store: store,
$route: {
params: {
taskID: 12,
programID: 1
}
}
},
i18n: new VueI18N({
silentTranslationWarn: true
}),
stubs: {
'default-layout': EmptySlotComponent,
'nested-router': EmptySlotComponent,
RouterLink: RouterLinkStub
}
})
})
it('has beforeRouteUpdate hook', () => {
// how do i call beforeRouteUpdate now?
// cmp.vm.beforeRouteUpdate(...)
}
Has anyone some ideas about this?
UPDATE:
I created a minimal example with #vue/cli and Mocha + Chai as unit test tools, which can be found here: https://github.com/BerniWittmann/vue-test-navigation-guard-reproduction
Got it working, but with a kind of hacky solution.
my test now looks like this:
it('test', () => {
const beforeRouteUpdate = wrapper.vm.$options.beforeRouteUpdate
const $store = {
dispatch: () => {
}
}
spyOn($store, 'dispatch').and.returnValue({ then: (arg) => arg() })
let next = jasmine.createSpy('next')
render()
beforeRouteUpdate.call({ $store }, {
params: {
taskID: 1
}
}, {}, next)
expect($store.dispatch).toHaveBeenCalledWith('setActiveTask', 1)
expect(next).toHaveBeenCalled()
})
The navigation guard is available in wrapper.vm.$options.beforeRouteUpdate, but calling this I lost the context of this so I was not able to call this.$store.dispatch in the component navigation guard, thats why I needed to use the .call() method
Following code worked fine for me for testing route navigation guards.
const beforeRouteUpdate = wrapper.vm.$options.beforeRouteUpdate[0];
let nextFun = jest.fn();
beforeRouteUpdate.call(wrapper.vm , "toObj", "fromObj", nextFun);
testing route navigation guards git hub
how to test vue router beforeupdate navigation guard