Set default meta properties on Vue Router - vue.js

I have searched the documentation here, and whilst there is a meta property on the VueRouter object, it doesn't seem to be doing anything (there is no description on the actual property in the docuementation)...
Consider the following routes:
let routes = [
{
path: '/',
component: require('./views/Home').default
},
{
path: '/about',
component: require('./views/About').default,
meta: {
transitionColor: '#000' // Note this property
}
}
];
I would like to do something like this:
export default new VueRouter({
mode: 'history',
routes,
meta: {
transitionColor: '#fff'
}
});
The intention of the above code is to set the default $route.meta.transitionColor to #fff for all routes, and then allow the route to override this if it is supplied in the route level meta.
Is there a way to set default meta properties on Vue Router in this way?

To my knowledge, it does not exist. However, with navigation guards, you can achieve something very similar.
router.beforeEach((to, from, next) => {
// manipulate route (e.g. set meta)
});

NavGuards are pretty handy - here's an simple example how to achieve something like you requested but using another meta-property - here called in router/index.ts somewhere after createRouter was invoked. The example declares every page without an explicitly auth-declaration as private using the custom meta-property requiresAuth (and sets the page title afterwards, while we're on it):
router.beforeEach((to, from) => {
if (!Object.prototype.hasOwnProperty.call(to?.meta, "requiresAuth")) {
to.meta.requiresAuth = true;
}
document.title =
"APP BRAND - " + i18n.global.t(`component.appNav.${String(to.name)}`);
next();
});

Related

Dynamically add a route in a Nuxt3 middleware

I have a Nuxt3 project where I'd like to add new routes based on an API call to a database. For example, let's say a user navigates to /my-product-1. A route middleware will look into the database and if it finds an entry, it will return that a product page should be rendered (instead of a category page, for example).
This is what I came up with:
export default defineNuxtPlugin(() => {
const router = useRouter()
addRouteMiddleware('routing', async (to) => {
if (to.path == '/my-awesome-product') {
router.addRoute({
component: () => import('/pages/product.vue'),
name: to.path,
path: to.path
})
console.log(router.hasRoute(to.path)) // returns TRUE
}
}, { global: true })
})
To keep it simple, I excluded the API call from this example. The solution above works, but not on initial load of the route. The route is indeed added to the Vue Router (even on the first visit), however, when I go directly to that route, it shows a 404 and only if I don't reload the page on the client does it show the correct page when navigated to it for the second time.
I guess it has something to do with the router not being updated... I found the following example in a GitHub issue, however, I can't get it to work in Nuxt3 as (as far as I'm aware) it doesn't provide the next() method.
When I tried adding router.replace(to.path) below the router.addRoute line, I ended up in an infinite redirect loop.
// from https://github.com/vuejs/vue-router/issues/3660
// You need to trigger a redirect to resolve again so it includes the newly added
route:
let hasAdded = false;
router.beforeEach((to, from, next) => {
if (!hasAdded && to.path === "/route3") {
router.addRoute(
{
path: "/route3",
name: "route3",
component: () => import("#/views/Route3.vue")
}
);
hasAdded = true;
next('/route3');
return;
}
next();
});
How could I fix this issue, please?
Edit:
Based on a suggestion, I tried using navigateTo() as a replacement for the next() method from Vue Router. This, however, also doesn't work on the first navigation to the route.
let dynamicPages: { path: string, type: string }[] = []
export default defineNuxtRouteMiddleware((to, _from) => {
const router = useRouter()
router.addRoute({
path: to.path,
name: to.path,
component: () => import ('/pages/[[dynamic]]/product.vue')
})
if (!dynamicPages.some(route => route.path === to.path)) {
dynamicPages.push({
path: to.path,
type: 'product'
})
return navigateTo(to.fullPath)
}
})
I also came up with this code (which works like I wanted), however, I don't know whether it is the best solution.
export default defineNuxtPlugin(() => {
const router = useRouter()
let routes = []
router.beforeEach(async (to, _from, next) => {
const pageType = await getPageType(to.path) // api call
if (isDynamicPage(pageType)) {
router.addRoute({
path: to.path,
name: to.path,
component: () => import(`/pages/[[dynamic]]/product.vue`),
})
if (!routes.some(route => route.path === to.path)) {
routes.push({
path: to.path,
type: pageType,
})
next(to.fullPath)
return
}
}
next()
})
})
I suggest you use dynamic routing within /page directory structure - https://nuxt.com/docs/guide/directory-structure/pages#dynamic-routes
The [slug] concept is designed exactly for your usecase. You don't need to know all possible routes in advance. You just provide a placeholder and Nuxt will take care of resolving during runtime.
If you insist on resolving method called before each route change, the Nuxt's replacement for next() method you're looking for is navigateTo
https://nuxt.com/docs/api/utils/navigate-to
And I advise you to use route middleware and put your logic into /middleware/routeGuard.global.ts. It will be auto-executed upon every route resolving event. The file will contain:
export default defineNuxtRouteMiddleware((to, from) => {
// your route-resolving logic you wanna perform
if ( /* navigation should happen */ {
return navigateTo( /* your dynamic route */ )
}
// otherwise do nothing - code will flow and given to.path route will be resolved
})
EDIT: However, this would still need content inside /pages directory or some routes created via Vue Router. Because otherwise navigateTo will fail, as there would be no route to go.
Here is an example of one possible approach:
https://stackblitz.com/edit/github-8wz4sj
Based on pageType returned from API Nuxt route guard can dynamically re-route the original URL to a specific slug page.

Redirect to a route without the assigned base path in vue

I have a vue router implemented and the configuration looks something like this:
const router = new Router({
mode: "history",
base: '/base/path,
routes,
});
Then my routes looks something like:
const routes = [
{
path: "/products",
name: "products",
component: () => import("#/test/items/Index.vue"),
beforeEnter(routeTo, routeFrom, next) {
if (//SOME CONDITION THAT SHOULD MATCH){
next('url/without/base'); // ROUTE THAT SHOULD BE WITHOUT BASE
}
next()
},
},
]
in the above case scenario whenever the IF condition is fullfiled the router is redirected to localhost:8080/base/path/url/without/base.
Is there a way or option so that the specific route is loaded without the base path so it would look something like: localhost:8080/url/without/base
I don't think there's a direct option for this but it sounds like something you will need to use window.location rather than next() to redirect to since that route will fall outside of your router's routes definition.
You could probably accomplish this by stripping the base url from router.START_LOCATION (Vue Router v3.5.0+), something like this:
beforeEnter(routeTo, routeFrom, next) {
if (//SOME CONDITION THAT SHOULD MATCH) {
const url = `${router.START_LOCATION.replace(router.base, '')}/url/without/base`;
window.location.replace(url);
return;
}
next();
}

Use same page for multiple routes

I am trying to find out how to use the same page for multiple routes on a Nuxt.js with i18n module.
Basically I want this route: /product-category/:slug/in/:material to use the same page as /product-category/:slug
So far I have tried below, adding it to nuxt.config.js - but it doesn't work. It simply shows the _slug/_material/index.vue file.
router: {
extendRoutes (routes, resolve) {
routes.push({
path: '/product-category/:slug/in/:material',
component: resolve(__dirname, 'pages/product-category/_slug/index.vue')
})
}
},
Maybe because I am having the i18n module, maybe because I am doing something wrong.
This is my folder structure:
If I inspect my router.js file, I see the path shown twice:
This was my workaround, I just wish there was a simpler method. Plus it still works if you use nuxt i18n.
nuxt.config.js
router: {
extendRoutes (routes, resolve) {
const routesToAdd = [
{ // add your routes here
name: 'product-category-slug-material',
path: '/product-category/:slug/in/:material',
component: resolve(__dirname, 'pages/product-category/_slug/index.vue'), // which component should it resolve to?
chunkName: 'pages/product-category/_slug/_material/index' // this part is important if you want i18n to work
}
];
const existingRoutesToRemove = routesToAdd.map(route => route.name);
const generateRoutes = routes.filter((route) => {
return !existingRoutesToRemove.includes(route.name);
});
routesToAdd.forEach(({ name, path, component, chunkName }) => {
generateRoutes.push({
name,
path,
component,
chunkName
});
});
routes.splice(0, routes.length, ...generateRoutes); // set new array
}
},
You can use _.vue to catch everything.
If you do not know the depth of your URL structure, you can use _.vue to dynamically match nested paths. This will handle requests that do not match a more specific request.
Here you can find out more.

Can i access router query params when defining a route's meta data?

I'm using Vue Router and setting the meta object field, which is used to set the page title and description.
Right now I set up routes like this:
[...
{
path: '/page1',
component: Page1Component,
meta: {
title: 'Title for page1'
}
}
...]
and then synchronize this with the DOM:
router.beforeEach((to, from, next) => {
document.title = to.meta.title;
next();
});
One of my routes, I want to use a query string in the title, but I can't pass a function to the meta object. Is there a way this can be done, without defining the title in the component?
For example, what I'd want to do:
[...
{
path: '/page1',
component: Page1Component,
meta: (route) => {
title: `dynamic title is ${route.query.param}`
}
}
...]
VueRouter doesn't support setting a route's meta property to be a function like you're trying to do.
But you could have your title property be able to also be set as a function which takes in your route as a param:
{
path: '/page1',
component: Page1Component,
meta: {
title: route => `dynamic title is ${route.query.param}`
}
}
And then add a check in the beforeEach hook to set the document.title to the returned value of the title function, in the cases where it is a function:
router.beforeEach((to, from, next) => {
const { title } = to.meta;
document.title = typeof title === 'function' ? title(to) : title;
next();
});
As said in the vue router documentation, the to and from objects in navigation guards are both route object, like the $route variable accessible in the component.
Therefore, you can do this :
router.beforeEach((to, from, next) => {
document.title = to.query.param;
next();
});

VueJS: one component for many routes

I'm a newbie to the world of VueJS.
My app uses vue-router to handle with routes changing. This's how it's configured:
new Router({
routes: [{
path: "/",
component: Articles
}, {
path: "/articles/:category",
component: Articles
}]
});
As you can see, I've got 2 routes handled by the same component. The idea is very simple. When we're on a home page, show articles from any category. When we're on a category page, show articles from that category.
The question is, how a component can detect whether the route's changed, e.g, from home page to category page?
I've come with this solution:
function loadArticles() {};
Vue.component("Articles", {
mounted: loadArticles,
watch: { "$route": loadArticles }
});
But I think, that's a bad solution.
Here's what I have done on a little project of mine, I'm using the template to display single article
This is my router
'/post/:id': {
name: 'post',
component: require('./components/article.vue')
},
In my .vue file, I get the $route.params to get the id of the article then displayed it in my ready function.
$.get('api/posts/' + this.$route.params.id, function(data){
myPost.current = data;
})
You'll need to get the category parameter then rendered the template according to it.
It should look something like this:
watch: {
'$route' (to, from) {
if (this.$route.params.category) {
this.loadArticles(category);
} else {
this.loadArticles();
}
}
}
'watch' is recommended here, in docs.