Dynamically Import Vue Component Based On URL Params - vue.js

I'm trying to import a vue component based on a path entered into url parameters. So for instance, if <host>/<path> is entered into the browser, I want to import a vue component located at <path>.vue.
In my routes.js file, I have a route that will get nested paths:
{ path: 'object/:catchAll(.*)*', component: BaseObject }
And send it to BaseObject:
<template>
<div>
<component :is="componentFile" />
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
name: 'BaseObject',
data () {
return {
componentPath: '',
address: ''
}
},
methods: {
importComponent (path) {
return () => import(`./${path}.vue`)
}
},
computed: {
componentFile () {
return this.importComponent(this.componentPath)
}
},
created () {
const params = this.$route.params.catchAll
console.log(params)
this.address = params.pop()
this.componentPath = params.join('/')
}
}
</script>
When I navigate to http://localhost:8080/#/object/action, I'm expecting the component located at ./action.vue to be loaded. But this doesn't happen - instead I get the following errors:
runtime-core.esm-bundler.js?9e79:38 [Vue warn]: Invalid VNode type: undefined (undefined)
at <Anonymous>
at <BaseObject onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< null > >
at <RouterView>
at <QPageContainer>
at <QLayout view="lHh Lpr lFf" >
at <MainLayout onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< Proxy {$i18n: {…}, $t: ƒ, …} > >
at <RouterView>
at <App>
and
Uncaught (in promise) Error: Cannot find module './.vue'
at app.js:416
Does anyone know how this can be accomplished?

There are at least 2 problems with your code...
Your "catch all" route is defined as { path: 'object/:catchAll(.*)*', component: BaseObject } so if you navigate to URL http://localhost:8080/#/object/action the "object" part is matched and catchAll param will be an array containing single item "action". So the created hook will pop this single item, params array remains empty and componentPath will be empty too (this is reason for Cannot find module './.vue' error)
In Vue 3, the old async component syntax (() => import(``./${path}.vue``)) is deprecated. You should always use defineAsyncComponent helper when creating async components (and this is reason for Invalid VNode type: undefined Vue warning)
So you BaseObject should look like this:
<template>
<div>
<component :is="componentFile" />
</div>
</template>
<script>
import { defineComponent, defineAsyncComponent } from "vue";
export default defineComponent({
name: "BaseObject",
data() {
return {
componentPath: "",
address: "",
};
},
methods: {},
computed: {
componentFile() {
return defineAsyncComponent(() =>
import(`../components/${this.componentPath}.vue`)
);
},
},
created() {
const params = this.$route.params.catchAll;
console.log(this.$route.params);
// this.address = params.pop();
this.componentPath = params.join("/");
},
});
</script>
Working demo
Also note that defining "catch all" route like this is dangerous as it will match all routes as - /object/action, /object/action/dd, /object/action/dd/bb etc. and those components will not exist. So maybe it would be better to allow only one level of nesting...

I guess that you are using Webpack to bundle your application and resolve JS modules. As you can see in the docs https://webpack.js.org/api/module-methods/#import-1, import() returns a promise, it works asynchronous. You have to resolve that promise first, before using the component.
methods: {
async getComponentFile() {
const component = await this.importComponent(this.componentPath);
return component;
}
},

Don't hard easy solutions!
<component :is="componentFile" />
export default {
name: 'BaseObject',
components: {
firstComponent: () => import('...'),
secondComponent: () => import('...'),
thirdComponent: () => import('...')
}
computed: {
componentFile () {
return this.detectComponentBasedPath()
}
},
methods: {
detectComponentBasedPath () {
...
}
}
}
</script>

Related

Why action of Vuex returns a promise<pending>?

I have an action in Vuex actions which commit a mutation that it take a payload from the component, that is a number of the index for returning an object, it works fine on Vuex js file meaning that shows the selected item on the console, as I said it gets index from the payload,
but on the component, it gives me Promise <Pending>, why that's happening? for now, I do not use any API for my Nuxt/Vue app, but I will, and for now, I just want to know why this is happening and what is the best solution for solving this
Here my Vuex codes:
export const state = () => ({
articles: [
{
uid: 0,
img: 'https://raw.githubusercontent.com/muhammederdem/mini-player/master/img/1.jpg',
link: '/articles/1',
},
{
uid: 1,
img: 'https://raw.githubusercontent.com/muhammederdem/mini-player/master/img/2.jpg',
link: '/articles/2',
},
],
})
export const getters = {
getArticles(state) {
return state.articles
},
}
export const mutations = {
getSpeceficArticle(state, payload) {
return state.articles[payload]
},
}
export const actions = {
getSpeceficArticle({ commit }, payload) {
commit('getSpeceficArticle', payload)
},
}
and here my component codes:
<template>
<div class="article">
{{ getSpeceficArticle() }}
<div class="article__banner">
<img src="" alt="" />
</div>
<div class="article__text">
<p></p>
</div>
</div>
</template>
<script>
export default {
name: 'HomeArticlesArticle',
data() {
return {
item: '',
}
},
// computed: {},
methods: {
async getSpeceficArticle() {
return await this.$store.dispatch('articles/getSpeceficArticle', 0)
},
},
}
</script>
actions are used to update the state they are like mutations but the main difference between them is that actions can include some asynchronous tasks, if you want to get a specific article at given index you should use a getter named getArticleByIndex :
export const getters = {
getArticles(state) {
return state.articles
},
getArticleByIndex:: (state) => (index) => {
return state.articles[index]
}
}
then define a computed property called articleByIndex :
<script>
export default {
name: 'HomeArticlesArticle',
data() {
return {
item: '',
}
},
computed: {
articleByIndex(){
return this.$store.getters.articles.getArticleByIndex(0)
}
},
methods: {
},
}
</script>
#Mohammad if you find yourself using a lot of getters/actions etc from Vuex and they're starting to get a little wordy, you can bring in mapGetters from Vuex and rename your calls to something a little more convenient. So your script would become,
<script>
import { mapGetters } from 'vuex'
export default {
name: 'HomeArticlesArticle',
data() {
return {
item: '',
}
},
computed: {
articleByIndex(){
return this.getArticleByIndex(0)
}
},
methods: {
...mapGetters({
getArticleByIndex: 'articles/getArticleByIndex',
})
},
}
</script>
You can add ...mapGetters, ...mapActions to your computed section also.
since there is no web service call in vuex action, try to remove async and await keywords from the component.
Later when you add a webservice call than you can wrap action body in new Promise with resolve and reject and then you can use async and await in component. let me know if this works for you.

Getting access to varaibles when testing Vue with Jest

I am using the structure below in my Vue.js web application. I am now trying to implement testing to it. But when trying to test the exampleOfFunction it says that this.exampleOfData2 is undefined.
<template>
*Some HTML*
</template>
<script>
*Some Imports*
export default {
data() {
return {
exampleOfData1: [],
exampleOfData2: 100
},
methods: {
exampleOfFunction:function(){
if(this.exampleOfData2 === 100)
{
return false;
}
return true;
},
created() {
},
mounted() {
}
}
</script>
In my testfile I then try to access the code above and I succeed with console.log(FileToTest.data()); I can see the values of data and I can access the function with FileToTest.methods.exampleOfFunction(); but when I call the function it says that this.exampleOfData2 is undefined.
It looks like you're using the component options definition instead of the component instance in your tests.
You should be creating a wrapper by mounting the component, and then you could access the component method via wrapper.vm:
import { shallowMount } from '#vue/test-utils'
import FileToTest from '#/components/FileToTest.vue'
describe('FileToTest', () => {
it('exampleOfFunction returns false by default', () => {
const wrapper = shallowMount(FileToTest)
expect(wrapper.vm.exampleOfFunction()).toBe(false)
})
it('exampleOfFunction returns true when data is not 100', () => {
const wrapper = shallowMount(FileToTest)
wrapper.setData({ exampleOfData2: 0 })
expect(wrapper.vm.exampleOfFunction()).toBe(true)
})
})

Vue dynamic component template not working for promise

<template>
<component :is="myComponent" />
</template>
<script>
export default {
props: {
component: String,
},
data() {
return {
myComponent: '',
};
},
computed: {
loader() {
return () => import(`../components/${this.component}`);
},
},
created() {
this.loader().then(res => {
// components can be defined as a function that returns a promise;
this.myComponent = () => this.loader();
},
},
}
</script>
Reference:
https://medium.com/scrumpy/dynamic-component-templates-with-vue-js-d9236ab183bb
Vue js import components dynamically
Console throw error "this.loader() is not a function" or "this.loader().then" is not a function.
Not sure why you're seeing that error, as loader is clearly defined as a computed prop that returns a function.
However, the created hook seems to call loader() twice (the second call is unnecessary). That could be simplified:
export default {
created() {
// Option 1
this.loader().then(res => this.myComponent = res)
// Option 2
this.myComponent = () => this.loader()
}
}
demo 1
Even simpler would be to rename loader with myComponent, getting rid of the myComponent data property:
export default {
//data() {
// return {
// myComponent: '',
// };
//},
computed: {
//loader() {
myComponent() {
return () => import(`../components/${this.component}`);
},
},
}
demo 2

vue.js 2 single file component with dynamic template

I need a single file component to load its template via AJAX.
I search a while for a solution and found some hints about dynamic components.
I crafted a combination of a parent component which imports a child component and renders the child with a dynamic template.
Child component is this:
<template>
<div>placeholder</div>
</template>
<script>
import SomeOtherComponent from './some-other-component.vue';
export default {
name: 'child-component',
components: {
'some-other-component': SomeOtherComponent,
},
};
</script>
Parent component is this
<template>
<component v-if='componentTemplate' :is="dynamicComponent && {template: componentTemplate}"></component>
</template>
<script>
import Axios from 'axios';
import ChildComponent from './child-component.vue';
export default {
name: 'parent-component',
components: {
'child-component': ChildComponent,
},
data() {
return {
dynamicComponent: 'child-component',
componentTemplate: null,
};
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>
The dynamic template code fetched is this:
<div>
<h1>My dynamic template</h1>
<some-other-component></some-other-component>
</div>
In general the component gets its template as expected and binds to it.
But when there are other components used in this dynamic template (some-other-component) they are not recognized, even if they are correctly registered inside the child component and of course correctly named as 'some-other-component'.
I get this error: [Vue warn]: Unknown custom element: some-other-component - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Do I miss something or is it some kind of issue/bug?
I answer my question myself, because I found an alternative solution after reading a little bit further here https://forum.vuejs.org/t/load-html-code-that-uses-some-vue-js-code-in-it-via-ajax-request/25006/3.
The problem in my code seems to be this logical expression :is="dynamicComponent && {template: componentTemplate}". I found this approach somewhere in the internet.
The original poster propably assumed that this causes the component "dynamicComponent" to be merged with {template: componentTemplate} which should override the template option only, leaving other component options as defined in the imported child-component.vue.
But it seems not to work as expected since && is a boolean operator and not a "object merge" operator. Please somebody prove me wrong, I am not a JavaScript expert after all.
Anyway the following approach works fine:
<template>
<component v-if='componentTemplate' :is="childComponent"></component>
</template>
<script>
import Axios from 'axios';
import SomeOtherComponent from "./some-other-component.vue";
export default {
name: 'parent-component',
components: {
'some-other-component': SomeOtherComponent,
},
data() {
return {
componentTemplate: null,
};
},
computed: {
childComponent() {
return {
template: this.componentTemplate,
components: this.$options.components,
};
},
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>

Refer `this` in App.vue cause undefined error

My App.vue file:
<template>
<v-app app color="grey">
<Navbar v-if="isLoggedIn"></Navbar>
<v-content class="mx-4 mb-4">
<router-view></router-view>
</v-content>
</v-app>
</template>
<script>
import Navbar from "./components/Navbar";
export default {
name: "App",
components: { Navbar },
data: () => {
return {
loggedIn: true
};
},
computed: {
isLoggedIn: () => this.data.loggedIn
},
mounted() {
console.log(process.env);
},
created() {}
};
</script>
Loading the app in browser I got the error message :
vue.runtime.esm.js?2b0e:619 [Vue warn]: Error in render: "TypeError: Cannot read property 'data' of undefined"
Screenshot:
Change this:
computed: {
isLoggedIn: () => this.data.loggedIn
}
to
computed: {
isLoggedIn() {
return this.loggedIn;
}
}
Don't use an es6 arrow function or this will lose access to the proper object. Here is a good answer on Stack Overflow about that.
this.data has to be used as this.$data (otherwise it's looking for a data property on the data property), but don't do that either: you actually don't need to reference data at all, and can go directly to the variable
Using a arrow function creates a anonymous function which means this is bound to window and not the vue instance. Using function ensures the value of this is the vue instance. So change the isLoggedIn computed property to:
computed: {
isLoggedIn: function() { return this.data.loggedIn }
}
// or
computed: {
isLoggedIn() { return this.data.loggedIn }
}