Routes.js call function from composable - laravel-8

I am working on my routes.js and I want to check if user has access to module before redirecting them by calling the function hasAccess() from a composable but it's giving me this warning:
[Vue warn]: inject() can only be used inside setup() or functional components.
routes.js
import { createWebHistory, createRouter } from "vue-router";
import useFN from "./composables/FN"
const routes = [
// define routes here
]
const router = createRouter({
history: createWebHistory(),
routes,
})
const { hasAccess } = useFN() // use function from composable
router.beforeEach(async (to, from, next) => {
console.log(await hasAccess()); // call function from composable
next()
}
export default router;
How can I call a function from composable if I don't have setup()?

Usually I don't have any issues using composables I create for my self as they are usually very basic, but according to Vue 3 - inject() can only be used inside setup() or functional components
composable your composable appears to be using provide/inject which is intended within a setup() method
I will like to propose another approach if viable.
You could call a method(action), getter inside your store instead, if you are using one.
I often use that perform checks before navigating to a route for example if a user account is suspended or the authenticated user does not have the intended privilege
import { createRouter, createWebHistory } from 'vue-router'
import store from '#/store'
...
const account = {
'ACTIVE': 1,
'RESTRICTED': 2,
'DECLINED': 0
}
const routes = [
{
path: '/some-path',
component: () => import('../components/...'),
props: route => ({ reference: route.query.reference }),
meta: { requiresAuth: true },
beforeEnter: async (to, from) => {
if (store.getters['Auth/authUser'].account_status == account.RESTRICTED) {
return false
}
return true
},
},
...
]
```

Related

Vue: Can't access Pinia Store in beforeEnter vue-router

I am using Vue 3 including the Composition API and additionally Pinia as State Management.
In the options API there is a method beforeRouteEnter, which is built into the component itself. Unfortunately this method does not exist in the composition API. Here the code, which would have been in the beforeRouteEnter method, is written directly into the setup method. However, this means that the component is loaded and displayed first, then the code is executed and, if the check fails, the component is redirected to an error page, for example.
My idea was to make my check directly in the route configuration in the beforeEnter method of a route. However, I don't have access to the Pinia Store, which doesn't seem to be initialized yet, although it is called before in the main.js.
Console Log
Uncaught Error: [🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?
const pinia = createPinia()
app.use(pinia)
This will fail in production.
Router.js
import { useProcessStore } from "#/store/process";
const routes: Array<RouteRecordRaw> = [
{
path: "/processes/:id",
name: "ProcessView",
component: loadView("ProcessView", "processes/"),
beforeEnter: () => {
const processStore = useProcessStore();
console.log(processStore);
},
children: [
{
path: "steer",
name: "ProcessSteer",
component: loadView("ProcessSteer", "processes/")
},
{
path: "approve/:code",
name: "ProcessApprove",
component: loadView("ProcessApprove", "processes/")
}
]
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;
main.js
import { createApp } from "vue";
import "#/assets/bundle-bootstrap.css";
import App from "#/App.vue";
import { createPinia } from "pinia";
import router from "#/router";
import SvgIcon from "#/components/SvgIcon.vue";
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.use(router);
app.component("SvgIcon", SvgIcon);
router.isReady().then(() => {
app.mount("#app");
});
However, I don't have access to the Pinia Store, which doesn't seem to be initialized yet, although it is called before in the main.js
Before what? Pinia instance is created with const pinia = createPinia(); after the router module is imported - while it is imported, all side-effects including the call to createRouter() are executed. Once the router is created it begins it's initial navigation (on client - on server you need to trigger it with router.push()) - if you happen to be at URL matching the route with guard that is using Pinia store, the useProcessStore() happens before Pinia is created...
Using a store outside of a component
You have two options:
either you make sure that any useXXXStore() call happens after Pinia is created (createPinia()) and installed (app.use(pinia))
or you pass the Pinia instance into any useXXXStore() outside of component...
// store.js
import { createPinia } from "pinia";
const pinia = createPinia();
export default pinia;
// router.js
import pinia from "#/store.js";
import { useProcessStore } from "#/store/process";
const routes: Array<RouteRecordRaw> = [
{
path: "/processes/:id",
name: "ProcessView",
component: loadView("ProcessView", "processes/"),
beforeEnter: () => {
const processStore = useProcessStore(pinia ); // <-- passing Pinia instance directly
console.log(processStore);
},
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
});
export default router;
// main.js
import { createApp } from "vue";
import App from "#/App.vue";
import store from "#/store.js";
import router from "#/router";
const app = createApp(App);
app.use(store);
app.use(router);
router.isReady().then(() => {
app.mount("#app");
});
Hope this would be helpful.
Vue provide support for some functions in which we need store(outside of the components).
To fix this problem I just called the useStore() function inside the function provided by Vue(beforeEach) and it worked.
Reference : https://pinia.vuejs.org/core-concepts/outside-component-usage.html
Example :
import { useAuthStore } from "#/stores/auth";
.
.
.
.
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
router.beforeEach(async (to, from) => {
const authStore = useAuthStore();
// use authStore Here
});
I have same problem to access the store in "beforeEach" method for managing authorization.
I use this method in main.js, not in router.js. in router.js store is not accessible.
create pinia instance in piniCreate.js
//piniaCreate.js
import { createPinia } from "pinia";
const pinia = createPinia();
export default pinia;
after that create my store in mainStore.js
import { defineStore } from 'pinia'
export const mainStore = defineStore('counter', {
state: () => {
return {
user: {
isAuthenticated: isAuthen,
}
}
},
actions: {
login(result) {
//...
this.user.isAuthenticated = true;
} ,
logOff() {
this.user.isAuthenticated = false;
}
}
});
Then I used beforeEach method in the main.js
//main.js
import { createApp } from 'vue'
import App from './App.vue'
import pinia from "#/stores/piniaCreate";
import { mainStore } from '#/stores/mainStore';
import router from './router'
const app = createApp(App)
.use(pinia)
.use(router)
const store1 = mainStore();
router.beforeEach((from) => {
if (from.meta.requiresAuth && !store1.user.isAuthenticated) {
router.push({ name: 'login', query: { redirect: from.path } });
}
})
app.mount('#app');
You can pass the method in the second parameter of definestore:
store.js
export const useAppStore = defineStore('app', () => {
const state = reactive({
appName: 'App',
appLogo: ''
})
return {
...toRefs(state)
}
})
router.js
router.beforeEach((to, from, next) => {
const apppStore = useAppStore()
next()
})
I have resolved this by adding lazy loading
const routes = [
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]

How to test the access of protected route guard with in Vuejs?

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;
});

Problems with the work of preloader vue.js

I'm trying to make showing preloader when i go from one component to another. I use this preloader. I create file loader.js and write there:
import Vue from 'vue';
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/vue-loading.css';
Vue.use(Loading);
let loader = Vue.$loading.show({
loader: 'dots',
color: '#5D00FF',
zIndex: 999,
});
function loaderStart() {
loader;
}
function loaderEnd() {
loader.hide();
}
export default {loaderStart, loaderEnd}
loader,js i import to the index.js and there i write when i want to call loader start but it does not starting(withoun if in beforeResolve preloader is working). Here is index.js:
import Vue from 'vue'
import Router from 'vue-router'
import Authorization from '#/components/Authorization'
import Main from '#/components/Main'
import loader from './loader'
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'Main',
component: Main,
},
{
path: '/authorization',
name: 'Authorization',
component: Authorization
}
]
})
router.beforeResolve((to, from, next) => {
if(to.path) {
loader.loaderStart()
}
next()
});
router.afterEach((to, from) => {
loader.loaderEnd()
});
export default router;
Please, help me find the problem
Your current loader will appear just once because you called show method once as well. You need to invoke show method every loaderStart call and store the loader:
let loader = null;
function loaderStart() {
// it would be better to extract these values as constants
loader = Vue.$loading.show({
loader: 'dots',
color: '#5D00FF',
zIndex: 999,
});
}
function loaderEnd() {
loader.hide();
}
Probably you have some async components since you added loader to routing logic, so you should use the beforeEach hook instead of the beforeResolve one.
router.beforeEach((to, from, next) => {
loader.loaderStart()
next()
});
router.afterEach((to, from) => {
loader.loaderEnd()
});
Loader API docs (show method)
Vue-router guards
Vue-router navigation flow

Why isn't router.currentRoute.path reactive?

I have an app which is contained in this div:
<div id="app" v-bind:style='{backgroundColor: backgroundColor}'>
... the app ...
</div>
The routing is done following the example in the documentation (this is a webpack project):
import Vue from 'vue/dist/vue.js'
import VueRouter from 'vue-router'
import ComponentOne from './component1.vue'
import ComponentTwo from './component2.vue'
Vue.use(VueRouter)
const routes = [{
path: '/foo',
component: ComponentOne
},
{
path: '/bar',
component: ComponentTwo
}
]
const router = new VueRouter({
routes // short for `routes: routes`
})
const app = new Vue({
router,
data: {
day: "Monday"
},
computed: {
backgroundColor: function () {
console.log(JSON.stringify(router.currentRoute))
if (router.currentRoute.path == "/foo") {
return "green"
} else {
return "blue"
}
}
}
}).$mount('#app')
I wanted the background to be dependent on the current route (router.currentRoute.path).
But, the solution above does not work, because router.currentRoute.path is not detected by the Vue instance as having changed (is not reactive).
What is the correct way to access the dynamic router data from within the Vue instance?
The router object created via new VueRouter is not reactive because Vue has no way to know to watch and update any object outside of its scope.
Passing router in the Vue config object is what allows the current route to be watched, but you need to reference it via this.$route:
if (this.$route.path == "/foo") {
...
}
You can also access the entire router object via this.$router, but its data is not reactive.
And if you are using Vue 2 with composition api setup() approach you can do this:
import { computed } from '#vue/composition-api'
export default {
setup (props, context) {
const params = computed ( () => context.root.$route.params)
const path = computed( () => context.root.$route.path)
I found on Vue's documentation page that tracks the router using watch for transition animations. Not sure if this is a best practice but you can use to.path or from.path to grab the path instead.
// then, in the parent component,
// watch the `$route` to determine the transition to use
watch: {
'$route': (to, from) => {
const toDepth = to.path.split('/').length
const fromDepth = from.path.split('/').length
this.transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left'
}
}

Vue Router: Keep query parameter and use same view for children

I'm rewriting an existing Angular 1 application with Vue.
The application always needs to authenticate an user by locale, id and token before entering any views. Respecting the conventions of our API, I specified the token as a query parameter within my main parent route.
Coming from the existing Angular's UI router implementation I thought this is the way to go:
// main.js
new Vue({
el: '#app',
router,
store,
template: '<router-view name="main"></router-view>'
})
// router.js
const router = new Router({
mode: 'history',
routes: [
{
name: 'start',
path : '/:locale/:id', // /:locale/:id?token didn't work
query: {
token: null
},
beforeEnter (to, from, next) {
// 1. Get data from API via locale, id and token
// 2. Update store with user data
},
components: {
main: startComponent
},
children: [{
name: 'profile',
path: 'profile',
components: {
main: profileComponent
}
}]
}
]
})
When I navigate to the profile view, I expect the view to change and the query token to stay, e.g. /en-US/123?token=abc to /en-US/123/profile?token=abc. Neither happens.
I'm using Vue 2.3.3 and Vue Router 2.3.1.
Questions:
Can I keep query parameters when navigating to child routes?
Am I using the Vue router right here? Or do I need to blame my UI router bias?
You can resolve this in the global hooks of Router
import VueRouter from 'vue-router';
import routes from './routes';
const Router = new VueRouter({
mode: 'history',
routes
});
function hasQueryParams(route) {
return !!Object.keys(route.query).length
}
Router.beforeEach((to, from, next) => {
if(!hasQueryParams(to) && hasQueryParams(from)){
next({name: to.name, query: from.query});
} else {
next()
}
})
If the new route (to) does not have its own parameters, then they will be taken from the previous route (from)
You can add in a mounted hook a router navigation guard beforeEach like this preserveQueryParams:
// helpers.js
import isEmpty from 'lodash/isEmpty';
const preserveQueryParams = (to, from, next) => {
const usePreviousQueryParams = isEmpty(to.query) && !isEmpty(from.query);
if (usePreviousQueryParams) {
next({ ...to, query: from.query });
} else {
next();
}
};
// StartComponent.vue
removeBeforeEachRouteGuard: Function;
mounted() {
this.removeBeforeEachRouteGuard = this.$router.beforeEach(preserveQueryParams);
}
// don't forget to remove created guard
destroyed() {
this.removeBeforeEachRouteGuard();
// resetting query can be useful too
this.$router.push({ query: undefined });
}