path-to-regexp Find regular expression matching the route - vue.js

I am adding dynamic child components while the page loads. Adding child routes in OnCreated of parent does not make the page work when we refresh the page.
Hence, I am parsing the page templates (as I know them when the page loads).
I am now looking for a way to know the route which matches the href. Vue-js uses path-to-regexp and my question is very simple.
I want to know the matching component
const router = new VueRouter({
routes: [
// dynamic segments start with a colon
{ path: '/user/:id', component: User },
{ path: '/foo/bar', component: FooBar },
]
})
// Reverse of this
var matchingComponent = howDoIDothis(/foo/bar) // this should give me the matching
I need this so that I can remove from the path and then add the child component to the parent dynamically.

You need to use Vue router's router.getMatchedComponents method. However, this method needs that your router is fully initialized with all the routes. Otherwise, there is no way. Vue-router doesn't expose underlying parsed Regular expressions against which you can compare your href.
Also, what you are trying to do is not the most idiomatic way of doing things in Single Page Applications. A good practice is to declare all your routes upfront in some JS file using which you should initialize your router. Of course, you will want to protect certain routes for which you should use Route guards.
Finally, when you have all your routes declared upfront means you have all the components bundled upfront in one big JS file. To avoid this, wrap your component in async wrappers and bundler like Webpack would be smart enough to split the bundle into multiple smaller files.

Related

Vue-Router add new route dynamically when there is a new component

I need to create a fully modular Vue project and I can't figure out how do I add or remove routes when there is a new component or a component is deleted.
Now I statically type all the routes that are available in my Sidebar menu. Let's say there are 10 menus in my sidebar, it means that I should type 10 routes in my routes.js file.
But I need this project to be modular. By saying modular what I mean is, I need to give the component list to the project, I think that I should do that with a .json file and let's say this file says that it has only 5 component available, then the related routes will be dynamically created in the routes.js file, and when one of them is deleted, the related route should be removed from the routes list as well. That is what I want.
I want to do that because I develop a single frontend with Vue for more than one device. These devices may have different options and menus, when the device and the frontend matches, only the available menus will be put in the sidebar, so I can't type all the routes statically as I've been doing the whole time.
Any advice would be appreciated.
You can loop through a list of routes and use the router.addRoute function to dynamically add them to your router instance (requires vue-router 3.5.0+), instead of loading them by default. You could do this from the created() hook of your main layout for example.
// app.vue
<template>
<router-view />
</template>
<script>
export default {
created() {
let routes = loadRoutes(); // insert your own function to get the correct routes here
routes.forEach(route => this.$router.addRoute(route))
},
};
</script>
If you need to be able to switch between different layouts on the same device (like a shared terminal for managers and employees) you can reset the routes array by creating a new router instance, or removeRoute (if you use v4.0+).

Vuepress dynamic routes and render as page?

I use vuepress as a mixed solution of static generated pages and dynamic pages in the SPA approach. The dynamic data source is a large database server, hence it can not use the additionalPages feature introduced in vurepress 1.x. That means dynamic routes was added using enhanceApp.js as below. Dynamic pages are rendered with extended layouts to share same page structure (header, footer).
// Foo is a layout component extends from Layout.vue
import Foo from './layouts/Foo.vue'
export default ({
Vue, // the version of Vue being used in the VuePress app
options, // the options for the root Vue instance
router, // the router instance for the app
siteData // site metadata
}) => {
router.addRoutes([
{ path: '/foo/:id', component: Foo },
]);
}
It works, however the layouts/Foo.vue is a component. It missing frontmatter, markdown syntax like normal markdown page does. The question is how to load a markdown page and pass to routes as component?
I'm still new to Vuepress, but I come across this comment in the source code that might be able to help you.
When Vue SFCs are source files, make them as layout components directly
I haven't finished reading all source code, so I'm not very sure I understand it correctly. But I guess when Vuepress find a vue component, it will not render it like a markdown file. It assumes you have your own style inside the .vue file. So I think that may be why you are missing what normal markdown page has.
However, if you need to mix a dynamic data source with static pages, you can try to use Vue in markdown to get what you want to achieve.

vuejs loading entire .vue file dynamically from 1 template into another

Sorry if this is a repeat - I have had a look around and spent most of today on this issue without getting result I am after.
https://github.com/vahidhedayati/micronaut-vuejs-cqrs/blob/master/frontend/src/components/sample/dynamicForm/PropertySplit.vue#L29
https://github.com/vahidhedayati/micronaut-vuejs-cqrs/blob/master/frontend/src/components/sample/dynamicForm2/Property2.vue#L31
I have two similar vue parent pages that load up dynamic form content, what I am trying to do is to get the actual form content which resides on a totally different .vue file in each of the sub folders above.
If I use Vue.component('form-content' this does work but I end up with 1 instance of the form for both different calls rather than a dynamic form per call. So one of the two forms gets loaded in and reused on both calls.
I have tried to use a const variable instead of Vue.component which works correctly locally on the main vue page that loads in the const variable but I haven't been able to get child page to load up the const value
//import Vue from 'vue'
//Vue.component('form-content', {
// components: { ActualForm },
// template: `<actual-form></actual-form>`
//});
//const content = {
// components: { ActualForm },
// template: `<actual-form></actual-form>`
//}
ActualForm.vue is another page and ideally I want to be able to pass this dyanmically per call on different page to the underlying DynamicForm.vue which will load title etc and then the form as per vue page that is passed in.
It seems trivial not quite sure why I am struggling with this.
You cannot pass a component to another component through a prop. For that you need to use a slot. So your code would look like:
<dynamic-form
:headingText="currentHeadingText"
:sectionHeadingText="currentSectionHeadingText"
>
<actual-form/>
</dynamic-form>
And in the <dynamic-form> component you would write:
<template>
[…]
<slot></slot>
[…]
</template
Then the <actual-form> component would be included at the position of the <slot>-tag. You can also use multiple slots and pass in multiple components / template segments. (Read the docs on vuejs.org for detailed information)
I am not completely sure what you are trying to do though. Maybe you need to structure your application in a different way. E.g. use the <dynamic-form> in the template of the <actual-form>. You could then still pass props to <dynamic-form> and fill slots therein from the <actual-form>.

load routes via component from external api and add them to the router

I would like to load my routes from an external API. Some users might not have the permissions to access a module.
So my navbar makes an API call and gets all the modules returned. These module objects contain the path to the view file.
I tried to create a small sandbox to reproduce the problem
https://codesandbox.io/s/vue-routing-example-i5z1h
If you open this url in your browser
https://i5z1h.codesandbox.io/#/First
you will first get the following error
Url /First not found
but after clicking on the First module link in the navbar, the First view should get rendered.
I think the problem is related to the fact that the page has not yet started the navigation created event after loading and the module page is therefore not found. After changing a router URL the navigation component had enough time to add all the required routes to the router.
How can I load these URLs before the router leads to the first route and responds a 404 error?
The key idea here is to load the routes asynchronously which means you must defer loading of your SPA till that time. In your index.js or main.js, your code would be something like this:
// Some functions are assumed and not defined in the below code.
import Vue from 'vue';
import VueRouter from 'vue-router';
// Application root component
import App from './App.vue';
import { getRoutes } from './api';
// Register Vue plugins
Vue.use(VueRouter);
// Make API call here
// Some animation before the app is fully rendered.
showLoader();
getRoutes(/* Optional User data */)
.then((routesData) => {
// Stop the animation
stopLoader();
return routesData;
})
.then((routesData) => {
// processRoutes returns an array of `RouteConfig`
const routes = processRoutes(routesData);
const router = new Router({
routes: [
...routes,
{
path: '*',
component: NotFound
}
]
});
})
.then((router) => {
const app = new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
});
});
Additionally, there are a few things you need to do:
Routing is generally the higher-level concern. So if you consider DIP - Dependency Inversion and the stateful + singleton nature of the router, then it makes sense to bootstrap it at the very beginning. Thus, anything that router needs should be available. This means that the navbar component should not be responsible for making the API call. You must take it out.
Another possible solution is to use $router.addRoutes() method. But it is inadequate for your needs. It will not work considering authorization in mind. It will not prevent navigation.
On a philosophical level, when you are using SPA with client-side routing, then client-side routing is its own source of truth. It is reasonable to know all the routes upfront and hence most routers are designed with this idea in mind. Thus, a requirement like this is a poor fit for this paradigm. If you need something like this, then a server should possess the knowledge of client-side routes and during page refresh, the server should decide what to do - Load the SPA or reject with 404/403 page. And if the access is allowed, the server should inject routing data in the HTML page which will then be picked by Vue.js on the browser side. Many sophisticated SSR - Server-Side Rendering techniques exist to achieve this.
Alternative strategy: Use guards
Define all the routes upfront in your router for all the possible views of all the users.
Define guards for each authorized routes. All these guards would be resolved asynchronously.
Instead of loading routing data from API, use the API to return an Authorization Matrix. Use this API response in your route guards to determine the access.
To prevent calls to the same API multiple times, you can use some sort of caching like Proxy, Memoization, store etc. Generally, for a user, the Auth Matrix will not vary between the calls.
As an advantage of this, you can still load the application partially if required leading to meaningful user experience by reducing the user's time to interact with the application.

How to properly fetch data from API in vuex?

I'm working on my Vue.js application and having a trouble with fetching data via API with vuex-router-sync.
As I saw in every tutorial or sample, it is common thing to dispatch the store action on created component hook. In my case it doesn't seem to be an option and here's why:
I use the standard vue-router for my routing, and when I navigate between pages not only my content should change, but also my sidebar and header. Thus I implemented the named router-view concept, such as
routes: [{
path: '/',
components: {
page: Home,
sidebar: GeneralSidebar,
header: HomeHeader
}
}, {
path: '/game/:id',
name: 'game',
components: {
page: Game,
sidebar: GameSidebar,
header: GameHeader
}
}]
But the Game, GameHeader and GameSidebar should share the same getter for the currently selected game. It's impossible to decide, which one of those components should be dispatching the action to fetch the data.
I tried to hook on the router itself, on beforeEnter, but faced the issue, that navigating between the same routes (in my case from /game/1 to /game/2) does not trigger the beforeEnter.
Is there any way that I can hook on any route navigation? Or maybe a better pattern for dispatching fetch-actions?
Thanks in advance!
There are many ways to do this. For example You could fetch data inside some component which is loaded by route change and after set data to vuex by dispatching change. This does the job perfectly. Also keep in mind that there are few ways to fetch data inside any component - You can hook that in any of Vue lifecycle hooks. Take a look here https://router.vuejs.org/en/advanced/data-fetching.html
Also sometimes You need some data upfront of any route change and here You can use the same approach - make some request when Vue app is loaded.