dynamic importing components based on route - vue.js

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')
}
}
}

Related

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>

Is there any way to pass mixin to component loaded through Vue Router

I have a mixin which contains beforeCreate lifecycle event.
I would like to import that mixin only into certain components, which are directly loaded through router. I don't want to go into each one of them and manually import the mixin, and I would also want to avoid loading it globally.
I believe that the proper way to do it is in route options, possibly overriding the component method, or by adding mixin option for the route (alongside props, meta...).
I requested this new feature, but I guess I was misunderstood, or I didn't understand the proposed solution.
I tried to create main Vue instance and extend it in my components, but the method only executed from the main component.
Is there any way to make this work?
Example of project code is here
Perhaps I've misunderstood what you're asking but I'd have thought you could achieve this by extending the component:
import Vue from 'vue'
import Router from 'vue-router'
import MyMixin from './mixins/MyMixin'
import MyList from './components/MyList'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/list',
name: 'list',
component: {
extends: MyList,
mixins: [MyMixin]
}
}
// ...
]
})
So rather than using MyList directly it's being extended and the mixin added in.
Or if you've got a lot of them and want to avoid duplication you could do something like this:
export default new Router({
routes: [
{
path: '/list',
name: 'list',
doMagic: true,
component: MyList
}
// ...
].map(route => {
if (route.doMagic) {
route.component = {
extends: route.component,
mixins: [MyMixin]
}
}
return route
})
})
Here I've used a flag called doMagic to determine which components to modify but if you just wanted to change all of them then you wouldn't need such a flag.
That doesn't take nested routes into account but it could be adapted if required.
Likewise if you're using async components then you'll have to fiddle around with the promises but the core principle should be exactly the same.
Update:
Based on the example code provided, the following seems to work with lazily loaded components:
const routes = [
// ... routes defined as usual
];
const newRoutes = routes.map(route => {
const originalComponent = route.component;
let component = null;
if (typeof originalComponent === 'object') {
// Components that aren't lazily loaded
component = wrap(originalComponent);
} else {
// Components that are lazily loaded
component = async () => {
const module = await originalComponent();
return wrap(module.default || module);
}
}
return {
...route,
component
};
function wrap (cmp) {
return {
extends: cmp,
mixins: [MyMixin]
}
}
});
export default new Router({
routes: newRoutes
});

Vue test-utils how to test a router.push()

In my component , I have a method which will execute a router.push()
import router from "#/router";
// ...
export default {
// ...
methods: {
closeAlert: function() {
if (this.msgTypeContactForm == "success") {
router.push("/home");
} else {
return;
}
},
// ....
}
}
I want to test it...
I wrote the following specs..
it("should ... go to home page", async () => {
// given
const $route = {
name: "home"
},
options = {
...
mocks: {
$route
}
};
wrapper = mount(ContactForm, options);
const closeBtn = wrapper.find(".v-alert__dismissible");
closeBtn.trigger("click");
await wrapper.vm.$nextTick();
expect(alert.attributes().style).toBe("display: none;")
// router path '/home' to be called ?
});
1 - I get an error
console.error node_modules/#vue/test-utils/dist/vue-test-utils.js:15
[vue-test-utils]: could not overwrite property $route, this is usually caused by a plugin that has added the property asa read-only value
2 - How I should write the expect() to be sure that this /home route has been called
thanks for feedback
You are doing something that happens to work, but I believe is wrong, and also is causing you problems to test the router. You're importing the router in your component:
import router from "#/router";
Then calling its push right away:
router.push("/home");
I don't know how exactly you're installing the router, but usually you do something like:
new Vue({
router,
store,
i18n,
}).$mount('#app');
To install Vue plugins. I bet you're already doing this (in fact, is this mechanism that expose $route to your component). In the example, a vuex store and a reference to vue-i18n are also being installed.
This will expose a $router member in all your components. Instead of importing the router and calling its push directly, you could call it from this as $router:
this.$router.push("/home");
Now, thise makes testing easier, because you can pass a fake router to your component, when testing, via the mocks property, just as you're doing with $route already:
const push = jest.fn();
const $router = {
push: jest.fn(),
}
...
mocks: {
$route,
$router,
}
And then, in your test, you assert against push having been called:
expect(push).toHaveBeenCalledWith('/the-desired-path');
Assuming that you have setup the pre-requisities correctly and similar to this
Just use
it("should ... go to home page", async () => {
const $route = {
name: "home"
}
...
// router path '/home' to be called ?
expect(wrapper.vm.$route.name).toBe($route.name)
});

VueJS Adding to lifecycle hooks on every component

So I have a loader screen in my app, and the idea is to show the loader screen on the beforeCreate hook so the user can't see the stuff being rendered, and then on the mounted hook remove the loader screen.
This is fun and nice for when you have two or three view/components, but currently my app has a lot more than that, and adding it to each component/view doesn't make much sense for me.
So I was wondering, is there any way to add something to the beforeCreate and mounted hooks on a global scope. Something like this:
main.js
Vue.beforeCreate(() => {
//Show the loader screen
});
Vue.mounted(() => {
//Hide the loader screen
});
That way it would be applied to every component and view
You can use mixins for this purposes, and import in components.
//mixins.js
export default {
beforeCreate() {},
mounted() {}
}
And in component add mixins: [importedMixins]
You will have access to 'this'.
Actualy you can use and vuex to (mapGetters, mapActions etc.)
If you don't want include mixins in every component, try to use vue plugins system (https://v2.vuejs.org/v2/guide/plugins.html):
MyPlugin.install = function (Vue, options) {
// 1. add global method or property
Vue.myGlobalMethod = function () {
// something logic ...
}
// 2. add a global asset
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// something logic ...
}
...
})
// 3. inject some component options
Vue.mixin({
created: function () {
// something logic ...
}
...
})
// 4. add an instance method
Vue.prototype.$myMethod = function (methodOptions) {
// something logic ...
}
}
And use your plugin like this Vue.use(MyPlugin, { someOption: true })
There is something very silimar to your request in vue-router. I've never used afterEach but beforeEach works perfectly.
router.beforeEach((to, from, next) => {
/* must call `next` */
})
router.beforeResolve((to, from, next) => {
/* must call `next` */
})
router.afterEach((to, from) => {})
Here is a documentation
There is also a hook called 'beforeRouteEnter'.
Link to beforeRouteEnter docs

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

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.