Reputation: 1345
We are building an enormous website based on Vue and Nuxt with over 25 different page types that cannot be matched with standard /:id or /overview/:slug logic that comes out of the box with Vue Router.
As slug-matching isn't an option, we are thinking about the following solution:
topicPage
topicPage
relates to the nuxt page WpTopicPage
WpTopicPage
as our component within our wildcard instance of Vue RouterThis looks like the following in code:
export function createRouter() {
return new Router({
mode: 'history',
routes: [
// 1. User visits page "/this-is-a-topic-page"
{
name: 'wildcard',
path: '*',
component: *, // this should be dynamic
beforeEnter: (to, from, next) => {
// 2. Server calls API that returns the pageType `topicPage`
this.$axios.get(`/call-to-the-api?slug=${to.params.slug}`)
.then((res) => {
// 3. `topicPage` relates to the nuxt page `WpTopicPage`
if(res.data.pageType === 'topicPage') {
// 4. Set `WpTopicPage` as our Page component
return WpTopicPage;
}
})
},
},
],
});
}
The above obviously doesn't work. Is there a way to set the component
within a route dynamically in the beforeEnter function?
Upvotes: 7
Views: 2558
Reputation: 955
Our current solution does not violate the requirement of "read-only" to
and from
parameters in the guard, but it needs an access to the internals of the route to install a proxy, so it is less standard that the recommended method "substitute route and redirect to the substituted one". Yet, it can be useful if you do not want to rewrite the route tree each time.
First we install a guard to save the new route in a global variable. Global enough for the context of the router.
router.beforeEach( (to,from) => {nuevaRuta = to })
We save the to
, but it is still "read only". See how: we define the components and the props using proxies:
var nuevaRuta = 'None'
const proxyComponentes = {
get: function (target, prop, receiver) {
return FriendlyIframe;
},
ownKeys: (oTarget, sKey) => {
if (nuevaRuta == 'None' || typeof nuevaRuta.params.panes == 'undefined')
return Reflect.ownKeys(oTarget,sKey)
else {
return nuevaRuta.params.panes
}
},
getOwnPropertyDescriptor(target, prop) { // called for every property
return {
enumerable: true , configurable: true /* ...other flags, probable "value:..." */
};
}
};
const proxyPropiedades = {
get: function (target, prop, receiver) {
console.log(prop)
if (prop.startsWith('user_')){
var id = prop.replace('user_','')
return {src: `/u/${id}?noheader=true`}
}else{
return {src: `/p/${window.PRID}/${prop}?noheader=true`}
}
}};
var myComponents = {};
var myProps= {};
and we install them when building the route match. More precisely, for version 4 of the router, the proxy for components can be installed during the route definition:
routes:[
{
name: "generica",
path: '/:panes+',
components: new Proxy ( myComponents, proxyComponentes),
props: false
but props is "cached" via a normalisation, so we need to install it after. This is the most troubling detail, in my opinion
router.getRoutes().find( (e) => e.name=="generica").props = new Proxy (myProps, proxyPropiedades)
Of course, the method could stop working if the router implements caching and further optimisations in the processing of components. If you want a long term safe solution, keep updating and redirecting.
Upvotes: 0
Reputation: 3421
I've struggled some time ago with a similar task. I also needed a fully dynamic router but my app initialising sequence was a bit different.
new Vue({...})
), I have no routes and no related components.The cool thing is, it is possible to map and re-map the router at any point in time.
I think you can make use of my implementation even with your initialisation sequence.
So here is my router.js
import Vue from 'vue';
import Router from 'vue-router';
Vue.use(Router);
const createRouter = () =>
new Router({
mode: 'history',
linkActiveClass: 'active',
base: __dirname,
routes: []
});
const router = createRouter();
export function resetRouter() {
const newRouter = createRouter();
router.matcher = newRouter.matcher;
}
export default router;
Take note that there is the resetRouter
function. I think it is self-explanatory what it does.
As soon as your app knows what kind of routes and components need to be mapped/used, you can create the route collection and map them. Like so:
import { default as router, resetRouter } from '@/router';
// ...
let routes = [];
// some magic to fill the routes array
// I.e. items coming from API
items.forEach(item => {
if (item.view) {
const component = () => import(`@/views/${item.view}.vue`);
const route = {
name: null, // prevent duplicate named routes warning
path: item.path,
component,
meta: {
title: item.title
}
};
routes.push(route);
}
});
resetRouter();
router.addRoutes(routes);
Upvotes: 2
Reputation: 955
The two answers from @AndrewShmig and @fjemi are excellent ideas, and the only problem is that they do not agree with the expected design of Vue Router. It seems that Vue developers expect "to" to be static except perhaps for a meta field, and the function in component is expected to be a promise that will be cached after first use of the route.
The only way I can see agreeing with the documentation is to use the BeforeEnter hook to push the new component in a "/*" route, mark the meta field, and then return redirect. This will cause a re-entry in the hook, this time with the right component. It is a bit disturbing to keep replacing the route with each entry but it should not cause memory leaks, hopefully. And as we are substituting a pre-existent route, we could expect it to be optimised in the side of the router -but I have not checked-.
Upvotes: 0
Reputation: 17
Here is a solution.
Home.vue
and NotFound.vue
are manually set*.vue
) in the the views
directory.vue
file, except for Home.vue
, which is set at /
. Ex) the path for FileName.vue
is /filename
project structure
app
|__router
|__index.js
|__views
|__Home.vue
|__NotFound.vue
|__*.vue
index.js
import Vue from "vue";
import VueRouter from "vue-router";
// list of files in the `views` directory
const views = require.context(
`../views`,
true,
/^.*\.vue$/
)
Vue.use(VueRouter);
// routes
const routes = [{
path: "/",
name: "Home",
component: import("../views/Home.vue")
}]
// dynamically add all routes in views directory
for (var i = 0; i < views.keys().length; i++) {
// skip home and notfound since it is defined above
if (
views.keys()[i].slice(2, -4) !== 'Home' &&
views.keys()[i].slice(2, -4) !== 'NotFound'
) {
// file path for the route
let filePath = `views/${views.keys()[i].slice(2, -4)}.vue`
// add routes
routes.push({
path: views.keys()[i].slice(1, -4).toLowerCase(),
name: views.keys()[i].slice(2, -4),
component: () =>
import("../" + filePath)
})
}
}
// directs any undefined route to not found
routes.push({
path: "*",
name: "NotFound",
component: () => import("../views/NotFound.vue")
})
const router = new VueRouter({
routes: routes
});
export default router
Upvotes: 0
Reputation: 4923
It's possible to do. I have created a codepen for you to test:
Here it is:
Vue.use(VueRouter);
let A = {
mounted() {
console.log('Mouted component A');
},
};
let B = {
mounted() {
console.log('Mouted component B');
},
};
let C = {
mounted() {
console.log('Mouted component C');
},
};
const router = new VueRouter({
mode: "hash",
routes: [
{
path: '*',
beforeEnter(to, from, next) {
let components = {
default: [A, B, C][Math.floor(Math.random() * 100) % 3],
};
to.matched[0].components = components;
next();
}
},
]
});
app = new Vue({
router,
el: '#app',
components: { A, B, C }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.2/vue-router.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<router-link :to="'/' + Math.random()">anything</router-link>
<router-view></router-view>
</div>
This is the output:
As you can see in the console logs - each time something changes we get random component loaded and mounted.
Upvotes: 3