is it possible to specify which component should be used on router.go() in VueJS - vue.js

In VueJS im trying to setup a scenario where the component used is determined by the url path without having to statically map it.
e.g.
router.beforeEach(({ to, next }) => {
FetchService.fetch(api_base+to.path)
.then((response) => {
router.app.$root.page = response
// I'd like to specify a path and component on the fly
// instead of having to map it
router.go({path: to.path, component: response.pageComponent})
})
.catch((err) => {
router.go({name: '404'})
})
})
Basically, I'd like to be able to create a route on the fly instead of statically specifying the path and component in the router.map
Hope that make sense. Any help would be appreciated.

I think that what you're trying to archive is programmatically load some component based on the current route.
I'm not sure if this is the recommended solution, but is what comes to my mind.
Create a DynamicLoader component whit a component as template
<template>
<component :is="CurrentComponent" />
</template>
Create a watch on $route to load new component in each route change
<script>
export default {
data() {
return {
CurrentComponent: undefined
}
},
watch: {
'$route' (to, from) {
let componentName = to.params.ComponentName;
this.CurrentComponent = require(`components/${componentName}`);
}
},
beforeMount() {
let componentName = this.$route.params.ComponentName;
this.CurrentComponent = require(`components/${componentName}`);
}
}
</script>
Register just this route on your router
{ path: '/:ComponentName', component: DynamicLoader }
In this example I'm assuming that all my componennt will be in components/ folder, in your example seems like you're calling an external service to get the real component location, that should work as well.
Let me know if this help you

As par the documentation of router.go, you either need path you want to redirect to or name of the route you want to redirect to. You don't the component.
Argument of router.go is either path in the form of:
{ path: '...' }
or name of route:
{
name: '...',
// params and query are optional
params: { ... },
query: { ... }
}
so you dont need to return component from your API, you can just return path or name of route, and use it to redirect to relevant page.
You can find more details here to create named routes using vue-router.

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.

Loading different components based on route param in Vue Router

I'm building an editor app which supports multiple design templates. Each design template has wildly different set of fields so they each has their own .vue file.
I'm trying to dynamically load the corresponding view component file based on params. So visiting /editor/yellow-on-black would load views/designs/yellow-on-black.vue etc.
I've been trying to do it like this
{
path: '/editor/:design',
component: () => {
return import(`../views/designs/${route.params.design}`)
}
}
But of course route is not defined. Any idea on how to work around this?
The route's component option is only evaluated once, so that won't work. Here's a solution using a Dynamic.vue view which uses a dynamic component based on the route param.
Use a simple route definition with route param. I changed the param name to dynamic:
import Dynamic from '#/views/Dynamic.vue';
{
path: "/editor/:dynamic",
component: Dynamic
}
Create a generic Dynamic.vue component that dynamically loads a component from the route param. It expects the param to be called dynamic:
<template>
<component v-if="c" :is="c" :key="c.__file"></component>
</template>
<script>
export default {
data: () => ({
c: null
}),
methods: {
updateComponent(param) {
// The dynamic import
import(`#/components/${param}.vue`).then(module => {
this.c = module.default;
})
}
},
beforeRouteEnter(to, from, next) {
// When first entering the route
next(vm => vm.updateComponent(to.params.dynamic));
},
beforeRouteUpdate(to, from, next) {
// When changing from one dynamic route to another
this.updateComponent(to.params.dynamic);
next();
}
}
</script>

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.

dynamic importing components based on route

I'm new to VueJS and I haven't found a possibility to load components based on route. For example:
page/:pageid
page/one
page/two
I have a component Page.vue
Within that component, I watch route changes. If the route is $pageid, then import and load component $pageid.
I've read this documentation: https://v2.vuejs.org/v2/guide/components-dynamic-async.html. But that's more focussed on lazy-loading. I don't see an example for dynamic importing and loading.
Regards, Peter
According to Dynamic Route Matching of vue router, you can access the url parameters via the params property of the $route object. In your case it would be $route.params.pageid so you can use it to dynamically change the content base on the pageid parameter in the url. Also note that on url change from say in your case page/one to page/two the same component would be used, so you would have to watch the $route object change and change your content dynamically.
watch: {
'$route' (to, from) {
// react to route changes...
}
}
Vue allows you to define your component as a factory function that
asynchronously resolves your component definition.
Since import() returns a promise, so you can register your async component by using:
export default {
components: {
'Alfa': () => import('#/components/Alfa'),
'Bravo': () => import('#/components/Bravo'),
'Charlie': () => import('#/components/Charlie')
}
}
Vue will only trigger the factory function when the component needs to
be rendered and will cache the result for future re-renders.
So your component will be load only when it need to be render.
And you can use dynamic component to render it by using:
<component :is='page'/>
and
export default {
computed: {
page () {
return 'Alfa'
}
}
}
If you already using vue-router you can directly use this in routes definition. See more in document here.
const router = new VueRouter({
routes: [{
path: '/alfa',
component: () => import('#/components/Alfa')
}, {
path: '/bravo',
component: () => import('#/components/Bravo')
}, {
path: '/charlie',
component: () => import('#/components/Charlie')
}]
})
As you can see this is dynamic importing but static registration (you have to provide the path to component.) which fit mostly in many situations. But if you want to use dynamic registration, you can return component directly instead of name see document here.
export default {
computed: {
page () {
return () => import('#/components/Alfa')
}
}
}

Resolve Vue component names on the fly

I'm searching for a way to resolve Vue (sfc) component names 'on the fly'. Basically I'm getting kind of component names from a backend and need to translate them into real component names on the frontend. Something like this:
<text/> => /components/TextElement.vue
or
<component v-bind:is="text"></component> => => /components/TextElement.vue
The idea is providing a global function that maps the backend names to the frontend names. I don't want to do this in every place where those components are used, so maybe there is a kind of hook to change the names dynamically?
Thanks a lot,
Boris
You could create a wrapper <component> that maps the component names and globally registers the specified component on the fly.
Create a component with a prop (e.g., named "xIs") for the <component>'s is property:
export default {
props: {
xIs: {
type: String,
required: true
}
},
};
Add a template that wraps <component>, and a data property (e.g., named "translatedName") that contains the mapped component name (from backend to front-end name):
export default {
data() {
return {
translatedName: ''
}
}
};
Add a watcher on the prop that will register a component by the name indicated in the prop value:
import componentNames from './component-names.json';
export default {
watch: {
xIs: {
immediate: true, // run handler immediately on current `xIs`
async handler(value) {
try {
const name = componentNames[value]; // 1. lookup real component name
if (name) {
Vue.component(name, () => import(`#/components/${name}`)); // 2. register component
this.translatedName = name; // 3. set name for
}
} catch (e) {
console.warn(`cannot resolve component name: ${value}`, e);
}
}
}
}
}