Vue router access meta data from router-link - vue-router

I use vue3 and vue-router-4, and am trying to hide some router links for unauthorized users:
Route file:
{
path: "/users/create",
name: "UserCreate",
meta: {
title: "Create user",
requiresAuth: true
},
component: () =>
import(/* webpackChunkName: "create" */ "#/views/Users/Create.vue")
}
As you can see I have requiresAuth: true meta in this route, so I want to hide this for guests.
I want to use something like this in my views (v-if part, which is not working):
<router-link v-if="route.meta.requiresAuth === isLoggedIn()" to="/users/create" class="nav-link"
>Create user</router-link>
Please advise how I can achieve this, and if it's not possible with meta fields - what is the preferred way to hide links.
P.S. Of course all validation and checks of accesses will be performed at the back-end side, but I still don't want to show user links which they can't view.

Keep in mind that this only hides the links, users can still access these routes through the URL. you need to use the before navigation guard in addition to hiding links:
Navigation guard :
//router.js
router.beforeEach((to,from,next)=>{
if ( to.matched.some(record => record.meta.requiresAuth) {
if( isLoggedIn()) {
next()
} else {
// if user is not logged what should happen
}
} else {
next()
}
})
Hide links if they require Auth and the user is not authenticated:
<router-link v-if="isLoggedIn()" to="/users/create" class="nav-link">
Create user
</router-link>

Related

Vue Router route query param added by beforeEnter lost after navigate into the same route

When navigating into the same route you are currently at, the route query page added by the before enter route guard is lost.
This is due to the fact that router link to object does not contain query page, and it does not go into before enter hook anymore, since it is already in the same route.
I figured out that you can add it in your router push or link button and you need to add this whenever you know the view contain query page.
Example:
this.$router.push({ name: 'routeName', query: { page: 1 } });
Question:
Is there an elegant way to handle this in route guard?
Which hook should I use so that route query page can be kept even user navigate into the same route?
Example code:
Route
// Sample route
const routes = [
{
path: 'test',
name: 'Test',
component: TestPage,
beforeEnter: testPageGuard,
},
];
Route Guard
// Test Page Guard
testPageGuard: (to, from, next) => {
const { page = null } = to.query;
let finalNext;
if (!page) {
finalNext = {
...to,
query: {
...to.query,
page: 1,
},
};
}
if (finalNext) {
next(finalNext);
} else {
next();
}
}
View
// TestPage.vue
<template>
<!-- The problem can be reproduce when clicking this link
when you are already in route '/test' -->
<router-link :to="{ name: 'Test'}">
Test
</router-link>
</template>
<script>
export default {
name: 'Test',
};
</script>
Solution Figured:
Add query page to router link
// TestPage.vue
<template>
<!-- query page is added here -->
<router-link :to="{ name: 'Test', query: { page: 1 } }">
Test
</router-link>
</template>
<script>...</script>
I figured out that there are two more ways to do this.
Solution 1: Update at "beforeRouteUpdate" Hook
beforeRouteUpdate triggers when query param changes even when in the same route.
Hence we can remove the beforeEnter guard and the extra page query in the route link, and do query param page adding at that particular page.
Example Code
View
// TestPage.vue
<template>
<!-- The problem can be reproduce when clicking this link
when you are already in route '/test' -->
<router-link :to="{ name: 'Test'}">
Test
</router-link>
</template>
<script>
export default {
name: 'Test',
// Solution here
beforeRouteUpdate(to, from, next) {
if (!Object.prototype.hasOwnProperty.call(to.query, 'page')) {
next({
...to,
query: {
// This line is used to retain other query if there is any
...to.query,
page: 1,
},
});
} else {
next();
}
},
};
</script>
Solution 2: Update at "beforeEach" Hook
When query param changes even when in the same route, it actually go through beforeEach hook also.
Hence we can remove the beforeEnter guard and the extra page query in the route link.
Add meta tag hasQueryParamPage for that route and do query param page adding in the global beforeEach hook.
This design has better reusability if you have other pages that require the query param page.
Example Code
Route
// Sample route
const routes = [
{
path: 'test',
name: 'Test',
component: TestPage,
// add meta tag
meta: { hasQueryParamPage: true },
},
];
Router
// router.js
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
...
});
// Solution here
router.beforeEach((to, from, next) => {
if (to.matched.some((record) => (record.meta.hasQueryParamPage))) {
let updatedNext = null;
if (!Object.prototype.hasOwnProperty.call(to.query, 'page')) {
updatedNext = {
...to,
query: {
// This line is used to retain other query if there is any
...to.query,
page: 1,
},
};
}
if (updatedNext) {
next(updatedNext);
return;
}
}
next();
});

How to Suppress Fetch Occurring in Parent View (Vue.js)

I have a View.js application that includes two views - Articles.vue and Article.vue - such that Article.vue is nested within Articles. Here is how these views behave:
When the Articles created() lifecycle event fires, this view fetches a list of accounts via HTTP and renders links to Article.vue within a v-for loop.
A user can then click on an Article in the Articles view. This links to the child Articles.vue, which then shows details of the Article.
The issue that I am having is that when step 2 occurs, the Articles view recreates (i.e. the created() event re-fires, and the Articles list is unnecessarily re-retrieved.) This causes an unwanted delay and screen flash that makes the app feel less responsive.
What I would like is for the Articles view to render only once, and not each time an Article is clicked on. How might I accomplish this?
I do get the desired behavior if I include the Articles component directly in the home page, instead of linking to it as a view from the home page. However, since the Articles view is only intended to appear when the user selects this functionality, this direct inclusion is not a viable approach.
Also, I have come upon keep-alive, which I see will prevent the parent route from re-rendering. However, keep-alive is be too broad, as I do want the parent view to reload if the parent vue link is clicked again (i.e. to retrieve new Articles); I just don't want the view to reload if the child vue link is clicked.
Possibly, the answer lies in Navigation Guards:
https://router.vuejs.org/guide/advanced/navigation-guards.html
However, I am new to Vue and am not able to parse documentation on this feature to arrive at my solution.
Following is the relevant code:
Articles
<template>
<div class="hello">
<br />
<div v-for="Article in Articles" :key="Article.ArticleID">
<router-link :to="{name: 'Article', params: {'ArticleID': Article.ArticleID}}">{{Article.ArticleName}}</router-link>
</div>
<br />
<router-view :key="$route.path"></router-view>
</div>
</template>
<script>
export default {
name: "Articles",
data() {
return {
Articles: []
}
},
mounted() {
this.getArticles();
},
methods: {
getArticles: function() {
console.log("Getting data");
const url = "http://localhost:8001/articles"
fetch(url)
.then( (response) => {
console.log("Converting data to json");
return response.json();
})
.then( (data) => {
if (typeof(data[0].ArticleID)=="undefined") {
console.log("No data found.");
} else
{
console.log("Data found");
this.Articles = data;
}
})
.catch( err => console.log("Error retrieving data: " + err));
}
}
}
</script>
Routes Section
{
path: "/articles",
name: "Articles",
component: () =>
import(/* webpackChunkName: "y" */ "../views/Articles.vue"),
children: [
{
path: "/article/:ArticleID",
name: "Article",
props: true,
component: () =>
import(/* webpackChunkName: "z" */ "../components/Article.vue")
}
]
}

VUE route redirect not closing modal from other page

I am using vue2 routes set up by another dev but I am trying to change the 'redirect' currently set to go to the 'home' page.
This issue is that there is a page which when you first enter the page (for the very first time) a bootstrap modal is displayed. The issue I'm having is if I enter this page of the first time then change the URL, the 'redirect' is working as expected BUT the modal is also still displaying and I don't know how to make it close.
routes.js
import {store} from './store/store';
import Cart from './components/Cart';
import Stock from './components/Stock';
import Home from './components/Home.vue';
import Review from './components/Review';
import Order from './components/Order.vue';
import OrderReport from './components/OrderReport';
import AccountDetails from './components/AccountDetails';
import ExistingOrders from './components/ExistingOrders';
export const routes = [
{path: '', component: Home},
{path: '/order', component: Order},
{
path: '/review', component: Review, children: [
{path: '', component: ExistingOrders},
{
path: ':id', component: OrderReport, beforeEnter: (to, from, next) => {
let orderCount = store.getters.orderPaginator.items.filter(item => item.id === +to.params.id).length;
next(orderCount > 0);
}
}
]
},
{path: '/account', component: AccountDetails},
{path: '/stock', component: Stock},
{path: '/cart', component: Cart},
{path: '*', redirect: '/order'}
];
If I change "redirect: '/order'" to "redirect: '/'" it goes to my home page as expected but with the modal displayed. Is there a way to close on the redirect?
Page where modal should be displayed
**Changed URL to 'orders' and clicked 'Enter'
Directed to the correct page (Home page) but modal still displayed. Is there a way I can change the :key using routes or would I have to use something like this.$forceUpdate(), although I have read that this may not work in vue2 and is really a nice thing to do.
What's my best approach? Just to mention I am new to vue2
I had success with this approach.
You can use a feature called Global Before Guards in Vue Router.
Inside router folder index.js file:
const router = createRouter({
// not relevant to the answer
history: createWebHistory(process.env.BASE_URL),
routes
})
router.beforeEach((to, from, next) => {
// remove modal backdrop if one exists
let modalBackground = document.querySelector('.modal-backdrop')
if (modalBackground) {
modalBackground.remove()
}
// do other stuff
next()
})
In my case I wanted to remove the modal backdrop but you can modify this to suit your needs.
Probably not the cleanest or best way to resolve the issue but the only way I could think of was to use the following in my 'home.vue' file
mounted() {
this.closeAccountSelectModal();
},
methods: {
closeAccountSelectModal() {
$('.modal ').modal('hide');
}
}
Used the class so all other pages which have modals will also be covered by it.
I know its too late but you can watch for route changes in components so when the route changes just close the modal
watch:{
$route (to, from){
// hide modal
}
}

Experiencing navbar flicker with Vue.js

I'm experiencing a flicker in my navbar before a function is evaluated to either true or false.
The function that needs to evaluate is the following:
export default {
methods: {
isAuthenticated () {
return this.$store.state.user.authenticated
}
},
data: () => {
return {
unauthenticated: [
{
title: 'Link1',
url: '/link1'
},
{
title: 'Link2',
url: '/link2'
},
{
title: 'Link3',
url: '/link3'
}
],
authenticated: [
{
title: 'otherLink1',
url: '/otherlink1'
},
{
title: 'otherLink2',
url: '/otherlink2'
},
{
title: 'otherLink3',
url: '/otherlink3'
}
]
}
}
}
And the navbar has the following:
<template v-if="isAuthenticated()">
<b-nav is-nav-bar>
<b-nav-item v-for="nav in authenticated" :key="nav.title" :href="nav.url">{{nav.title}}</b-nav-item>
</b-nav>
</template>
<template v-else>
<b-nav is-nav-bar>
<b-nav-item v-for="nav in unauthenticated" :key="nav.title" :href="nav.url">{{nav.title}}</b-nav-item>
</b-nav>
</template>
However, when I click through the navigation, the unauthenticated links appear for a second and then the authenticated links appear as if the isAuthenticated() function hasn't evaluated yet. What can I do to remove this flicker?
My store file (user.js) file looks like this:
export const state = () => ({
headers: {},
profile: {}
})
export const mutations = {
updateHeaders (state, headers) {
state.headers.access_token = headers['access-token']
state.headers.token_type = headers['token-type']
state.headers.client = headers['client']
state.headers.expiry = headers['expiry']
state.headers.uid = headers['uid']
if (state.headers.expiry == null) {
state.authenticated = false
} else {
let timeToExpiry = new Date(state.headers.expiry * 1000)
let now = new Date()
state.authenticated = now < timeToExpiry
}
},
signout (state) {
state.headers = {}
state.profile = {}
}
}
The login/logout methods occur via API calls to a Rails app. The Devise gem handles the rest.
Thanks in advance!
EDIT:
I am using Nuxt.js for the layouts/pages/components so I believe that links submit with a this.$router.push(url) under the hood.
The b-nav tags are coming from Bootstrap Vue
When using bootstrap-vue there are two ways to add links to the navbar. One is to bind to :href attribute, which creates a regular html anchor. The other is to use :to attribute, which creates a link that interacts with vue-router.
<b-navbar-nav v-if="isAuthenticated()">
<b-nav-item v-for="nav in authenticated" :key="nav.title" :to="nav.url">{{nav.title}}</b-nav-item>
</b-navbar-nav>
<b-navbar-nav v-if="!isAuthenticated()">
<b-nav-item v-for="nav in unauthenticated" :key="nav.title" :to="nav.url">{{nav.title}}</b-nav-item>
</b-navbar-nav>
No reason to use <template> tags here to encapsulate the . Also note that 'is-nav-bar' is deprecated. See here where they note the deprecation.
What code executes when you click one of the links is not stated, I assume it's something like this.$router.push(url). If this is the case, you've probably have included your navbar in the <router-view>, so when you switch current route, components inside <router-view> rerender, so the navbar flashes. Move them out of the <router-view> should fix this.
edit: so the OP is not using vue-router yet, in this case, either manually change the root component's data to make parts other than the navs change, or add vue-router and use this.$router.push() to navigate so parts outside <router-view> won't change or flash.
Anyway, we need the vue component to stay to let vue to rerender only part of the view, while simply navigating by <a> or something will destruct everything and reconstruct them again, hence the flashing.

Vue.js - two different components on same route

I'm trying to figure out how to have 2 different components on same route with Vue.
Main page or login page, depends if user is authenticated.
Maybe im missing something in documentation, but i cant and cant figure it out. Is it even possible?
Thx
So you need dynamic components.
in whichever Vue is a parent to these components use a computed property that returns the name of component you want to use, based on the authenticated state:
//- in your js
// insert into the vue instance definition, assuming you have an authencation
// property somewhere, eg session.isAuthenticated
...
components: {
MainComponent,
LoginComponent
},
computed: {
useComponent () {
return session.isAuthenticated ? 'MainComponent' : 'LoginComponent'
}
}
...
//- in your template html
<component :is='useComponent' />
http://vuejs.org/guide/components.html#Dynamic-Components
use auth param in router map:
router.map({
'/home': {
component: Home,
auth: true
},
'/login': {
component: Login
},
'/something': {
component: Something,
auth: true
},
})
and then check before each transition:
router.beforeEach(function (transition) {
if (transition.to.auth && !auth.user.authenticated) {
transition.redirect('/login')
} else {
transition.next()
}
})