How to create dynamic components in vueJS - vue.js

I am new to vueJS and am trying to load components dynamically. I searched on the web and experimented with many suggestions but am still not able to get it to work.
Scenario: I want to have a 'shell' component that acts as a container for swapping in and out other components based on user's selection. The file names of these components will be served up from a database
Here's the shell component:
<template>
<keep-alive>
<component :is='compName'></component>
</keep-alive>
</template>
<script>
props: ['vueFile'],
export default ({
data() {
compName: ()=> import('DefaultPage.vue')
},
watch: {
compName() {
return ()=> import(this.vueFile);
}
}
})
</script>
This does not work.
If I hard code the component file name, it works correctly... e.g.
return ()=> import('MyComponent.vue');
The moment I change the import statement to use a variable, it gives an error even though the variable contains the same string I hard code.
What am I doing wrong?

compName() {
const MyComponent = () => import("~/components/MyComponent.js");
}
You can see this post
https://vuedose.tips/dynamic-imports-in-vue-js-for-better-performance

You can put the components you want to add dynamically in a directory, for example: ./component, and try this
compName () {
return ()=> import(`./component/${this.vueFile}`);
}
The import() must contain at least some information about where the module is located.
https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import

Related

How to use VueRouter with Storybook

I'm trying to write a story for a component that references this.$route.params. I'm not sure how to synthetically define this.$route in the context of a story. I think the solution is to use decorators, but all the examples in the docs focus on rendering, like adding a wrapping <div> etc. I'm not sure how to inject values.
I also found this project which appears designed for this exact situation, but it hasn't been maintained in years and README references outdated syntax that doesn't match modern versions of Storybook, so I don't think it's an option.
Here's what doesn't work:
import AssetShow from '../app/javascript/src/site/components/assets/Show'
export default {
title: 'Site/AssetShow',
component: AssetShow,
parameters: {
}
};
export const CustomerSpotlight = () => ({
components: { AssetShow },
template: '<AssetShow />',
});
import Vue from 'vue'
import VueRouter from 'vue-router'
import StoryRouter from 'storybook-vue-router';
CustomerSpotlight.decorators = [
(story) => {
Vue.use(VueRouter)
return {
components: { story },
template: '<story />'
}
}
];
The component I'm writing the story for has this:
mounted() {
axios.get(`.../bla/${this.$route.params.id}.json`)
},
...which causes Storybook to throw this error: TypeError: Cannot read properties of undefined (reading 'params')
I suppose that your intention is to do something with the story's component based on the route parameters?
If that is the case, then I don't think you need to define the route.params within the story context. I suggest either keeping that code within the component itself, or create an option within the story for the user to simulate adding parameters to the path. Which you can simply have as an input text / select field that you send down to the component as a prop.

How do I properly import multiple components dynamically and use them in Nuxt?

I need to implement dynamic pages in a Nuxt + CMS bundle.
I send the URL to the server and if such a page exists I receive the data.
The data contains a list of components that I need to use, the number of components can be different.
I need to dynamically import these components and use them on the page.
I don't fully understand how I can properly import these components and use them.
I know that I can use the global registration of components, but in this case I am interested in dynamic imports.
Here is a demo that describes the approximate logic of my application.
https://codesandbox.io/s/dank-water-zvwmu?file=%2Fpages%2F_.vue
Here is a github issue that may be useful for you: https://github.com/nuxt/components/issues/227#issuecomment-902013353
I've used something like this before
<nuxt-dynamic :name="icon"></nuxt-dynamic>
to load dynamic SVG depending of the icon prop thanks to dynamic.
Since now, it is baked-in you should be able to do
<component :is="componentId" />
but it looks like it is costly in terms of performance.
This is of course based on Nuxt components and auto-importing them.
Also, if you want to import those from anywhere you wish, you can follow my answer here.
I used this solution. I get all the necessary data in the asyncData hook and then import the components in the created () hook
https://codesandbox.io/s/codesandbox-nuxt-uidc7?file=/pages/index.vue
asyncData({ route, redirect }) {
const dataFromServer = [
{
path: "/about",
componentName: 'myComponent'
},
];
const componentData = dataFromServer.find(
(data) => data.path === route.path
);
return { componentData };
},
data() {
return {
selectedRouteData: null,
componentData: {},
importedComponents: []
};
},
created() {
this.importComponent();
},
methods: {
async importComponent() {
const comp = await import(`~/folder/${this.componentData.componentName}.vue`);
this.importedComponents.push(comp.default);
}

How can I load an nested component dynamically in Vuejs without hardcoding the url

I followed Markus Oberlehner for loading Vue components via http. We have a Vue component precompiled as a .js file hosted on a separate server. When the user navigates to this loader component below, the call to externalComponent() successfully fetches the .js file and renders the component on this page. That is great. This only works if we hardcode the url in the loader component.
We are building a plugin architecture into our site. We have written some single page Vue component files. Each of these files is a plugin. We precomiled these .vue files into .js files according to Markus Oberlehner's helpful tutorial here: https://github.com/maoberlehner/distributed-vue-applications-loading-components-via-http.
We also have a Vue component in our main site - let's call it the loader component - that fetches a .js file and renders it into a component using the externalComponent() method - demonstrated in Markus's tutorial. This works, but since the MyComponent is a constant defined outside of the loader component's data object, we cannot dynamically inject the plugin_id of from the vue router into the .js file's url.
If you're curious why our urls don't end in .js it is because we are passing a url to an endpoint in our server instead. This endpoint fetches the .js file and returns it to our client.
<template>
<div>
<MyComponent />
</div>
</template>
<script>
import util from "~/js/util.js";
let MyComponent = () =>
/* If we hadcode the url, the page renders no problemo.
*
* util.externalComponent("http://localhost:8081/api/plugins/167/code");
*/
/* However, we'd like to fetch the plugin_id from the Vue router and inject that into the argument
* as I've tried to achieve in the line of code below.
*
* The following code does not work because Vue apparently loads the MyComponent element in the DOM
* before executing the created() hook. We get the error "plugin_id is not defined."
*/
util.externalComponent(
"http://localhost:8081/api/plugins/" + plugin_id + "/code"
);
export default {
name: "plugin",
components: {
MyComponent
},
data() {
return {
plugin_id: null
};
},
created() {
/* This line does indeed populate the plugin_id data variable, although this happens after the
* page attempts to load MyComponent
*/
this.plugin_id = this.$route.params.plugin_id;
}
};
</script>
So, how can we modify this code to dynamically insert the plugin_id into the url?
This is a nuxt project by the way.
Update
Here is an approach that works the first time I load the page, but consecutive loads are still a problem. Specifically, the first component just loads again regardless of whatever the new plugin id may be.
But this looks like the right direction...
<template>
<div>
<component v-bind:is="component" v-if="loadedUrl"></component>
{{ plugin_id }}
</div>
</template>
<script>
import util from "~/js/util.js";
export default {
name: "plugin",
props: [],
data() {
return {
loadedUrl: false,
component: null
};
},
beforeRouteEnter(to, from, next) {
next(vm => {
vm.loadedUrl = false;
vm.component = () =>
util.externalComponent(
"http://localhost:8081/api/plugins/" + to.params.plugin_id + "/code"
);
vm.loadedUrl = true;
next();
});
},
beforeDestroy() {
//does not seem to help.
console.log("in beforeDestroy");
this.component = null;
}
};
</script>

Same VueX store module registered from components on two pages

I have encountered a weird case when using VueX and Vue-Router and I am not too sure how to cleanly solve it.
I have a component (let's call it "ComponentWithStore") that registers a named store module a bit like this : (the actual content of the store don't matter. obviously in this toy example using VueX is overkill, but this is a very simplified version of a much more complexe app where using VueX makes sense)
// ComponentWithStore.vue
<script>
import module from './componentStore.js';
export default {
name: 'ComponentWithStore',
beforeCreate() {
this.$store.registerModule(module.name, module);
},
beforeDestroy() {
this.$store.unregisterModule(module.name);
}
}
</script>
Then I place this component in a view (or page) which is then associated to a route (let's call this page "Home").
// Home.vue
<template>
<div class="home">
Home
<ComponentWithStore/>
</div>
</template>
<script>
import ComponentWithStore from '#/components/ComponentWithStore.vue';
export default {
name: "Home",
components: { ComponentWithStore }
};
</script>
So far so good, when I visit the Home route, the store module is registered, and when I leave the Home route the store module is cleaned up.
Let's say I then create a new view (page), let's call it "About", and this new About page is basically identical to Home.vue, in that it also uses ComponentWithStore.
// About.vue
<template>
<div class="about">
About
<ComponentWithStore/>
</div>
</template>
<script>
import ComponentWithStore from '#/components/ComponentWithStore.vue';
export default {
name: "About",
components: { ComponentWithStore }
};
</script>
Now I encounter the following error when navigating from Home to About :
vuex.esm.js?2f62:709 [vuex] duplicate namespace myComponentStore/ for the namespaced module myComponentStore
What happens is that the store module for "About" is registered before the store module for "Home" is unregistered, hence the duplicate namespace error.
So I understand well what the issue is, however I am unsure what would be the cleanest solution to solve this situation. All ideas are welcome
A full sample may be found here : https://github.com/mmgagnon/vue-module-router-clash
To use, simply run it and switch between the Home and About pages.
As you have mentioned, the issue is due to the ordering of the hooks. You just need to use the correct hooks to ensure that the old component unregisters the module first before the new component registers it again.
At a high level, here is the order of hooks in your situation when navigating from Home to About:
About beforeCreate
About created
Home beforeDestroy
Home destroyed
About mounted
So you can register the module in the mounted hook and unregister it in either beforeDestroy or destroyed.
I haven't tested this though. It might not work if your component requires access to the store after it is created and before it is mounted.
A better approach is to create an abstraction to register and unregister modules that allows for overlaps.
Untested, but something like this might work:
function RegistrationPlugin(store) {
const modules = new Map()
store.registerModuleSafely = function (name, module) {
const count = modules.get(name) || 0
if (count === 0) {
store.registerModule(name, module)
}
modules.set(name, count + 1)
}
store.unregisterModuleSafely = function (name) {
const count = modules.get(name) || 0
if (count === 1) {
store.unregisterModule(name)
modules.delete(name)
} else if (count > 1) {
modules.set(name, count - 1)
}
}
}
Specify the plugin when you create your store:
const store = new Vuex.Store({
plugins: [RegistrationPlugin]
})
Now register and unregister your modules like this:
beforeCreate() {
this.$store.registerModuleSafely(module.name, module)
},
destroyed() {
this.$store.unregisterModuleSafely(module.name)
}

did you register the component correctly? For recursive components, make sure to provide the "name" option

I configured 'i-tab-pane': Tabpane but report error,the code is bellow:
<template>
<div class="page-common">
<i-tabs>
<i-tab-pane label="wx">
content
</i-tab-pane>
</i-tabs>
</div>
</template>
<script>
import {
Tabs,
Tabpane
} from 'iview'
export default{
name:"data-center",
data(){
return {msg: 'hello vue'}
},
components: {
'i-tabs' : Tabs,
'i-tab-pane': Tabpane
}
}
</script>
Error traceback:
[Vue warn]: Unknown custom element: <i-tab-pane> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
found in
---> <DataCenter> at src/views/dc/data-center.vue
<Index> at src/views/index.vue
<App> at src/app.vue
I have tried in the main.js to global configuration:
Vue.component("Tabpane", Tabpane);
but still do not work.
How to resolve this issue?
If you're using a component within a component (e.g. something like this in the Vue DOM):
App
MyComponent
ADifferentComponent
MyComponent
Here the issue is that MyComponent is both the parent and child of itself. This throws Vue into a loop, with each component depending on the other.
There's a few solutions to this:
 1. Globally register MyComponent
vue.component("MyComponent", MyComponent)
2. Using beforeCreate
beforeCreate: function () {
this.$options.components.MyComponent = require('./MyComponent.vue').default
}
3. Move the import into a lambda function within the components object
components: {
MyComponent: () => import('./MyComponent.vue')
}
My preference is the third option, it's the simplest tweak and fixes the issue in my case.
More info: Vue.js Official Docs — Handling Edge Cases: Circular References Between Components
Note: if you choose method's 2 or 3, in my instance I had to use this method in both the parent and child components to stop this issue arising.
Since you have applied different name for the components:
components: {
'i-tabs' : Tabs,
'i-tab-pane': Tabpane
}
You also need to have same name while you export: (Check to name in your Tabpane component)
name: 'Tabpane'
From the error, what I can say is you have not defined the name in your component Tabpane. Make sure to verify the name and it should work fine with no error.
Wasted almost one hour, didn't find a solution, so I wanted to contribute =)
In my case, I was importing WRONGLY the component.. like below:
import { MyComponent } from './components/MyComponent'
But the CORRECT is (without curly braces):
import MyComponent from './components/MyComponent'
One of the mistakes is setting components as array instead of object!
This is wrong:
<script>
import ChildComponent from './ChildComponent.vue';
export default {
name: 'ParentComponent',
components: [
ChildComponent
],
props: {
...
}
};
</script>
This is correct:
<script>
import ChildComponent from './ChildComponent.vue';
export default {
name: 'ParentComponent',
components: {
ChildComponent
},
props: {
...
}
};
</script>
Note: for components that use other ("child") components, you must also specify a components field!
For recursive components that are not registered globally, it is essential to use not 'any name', but the EXACTLY same name as your component.
Let me give an example:
<template>
<li>{{tag.name}}
<ul v-if="tag.sub_tags && tag.sub_tags.length">
<app-tag v-for="subTag in tag.sub_tags" v-bind:tag="subTag" v-bind:key="subTag.name"></app-tag>
</ul>
</li>
</template>
<script>
export default {
name: "app-tag", // using EXACTLY this name is essential
components: {
},
props: ['tag'],
}
I had this error as well. I triple checked that names were correct.
However I got this error simply because I was not terminating the script tag.
<template>
<div>
<p>My Form</p>
<PageA></PageA>
</div>
</template>
<script>
import PageA from "./PageA.vue"
export default {
name: "MyForm",
components: {
PageA
}
}
Notice there is no </script> at the end.
So be sure to double check this.
If you have path to the component (which causes a cycle) to index.js, cycle will be begin. If you set path directly to component, cycle will be not. For example:
// WRONG:
import { BaseTable } from #/components/Base'; // link to index.js
// SUCCESS:
import BaseTable from #/components/Base/Table.vue';
I had this error and discovered the issue was because the name of the component was identical to the name of a prop.
import Control from '#/Control.vue';
export default {
name: 'Question',
components: {
Control
},
props: ['Control', 'source'],
I was using file components. I changed the Control.vue to InputControl.vue and this warning disappeared.
The high votes answer is right. You can checkout that you have applied different name for the components. But if the question is still not resolved, you can make sure that you have register the component only once.
components: {
IMContainer,
RightPanel
},
methods: {},
components: {
IMContainer,
RightPanel
}
we always forget that we have register the component before
This is very common error that we face while starting any Project Vue. I spent lot of time to search this error and finally found a Solution.
Suppose i have component that is "table.vue",
i.e components/table.vue
In app.js
Vue.component('mytablecomp', require('./components/table.vue').default);
So in in your index.blade file call component as
<mytablecomp></mytablecomp>
Just you need to keep in mind that your component name is in small not in large or camel case.
Then my above code will surely work for you.
Thanks
We've struggled with this error twice now in our project with different components. Adding name: "MyComponent" (as instructed by the error message) to our imported component did not help. We were pretty sure our casing was correct, as we used what is in the documentation, which worked fine for the other 99% of our components.
This is what finally worked for us, just for those two problematic components:
Instead of this (which, again, works for most of our components):
import MyComponent from '#/components/MyComponent';
export default {
components: {
MyComponent
}
We changed it to ONLY this:
export default {
components: {
MyComponent: () => import('#/components/MyComponent')
}
I can't find the documentation we originally found for this solution, so if anyone has any references, feel free to comment.
If you are using Vue Class Component, to register a component "ComponentToRegister" you can do
import Vue from 'vue'
import Component from 'vue-class-component'
import ComponentToRegister from '#/components/ComponentToRegister.vue'
#Component({
components: {
ComponentToRegister
}
})
export default class HelloWorld extends Vue {}
Adding my scenario. Just in case someone has similar problem and not able to identify ACTUAL issue.
I was using vue splitpanes.
Previously it required only "Splitpanes", in latest version, they made another "Pane" component (as children of splitpanes).
Now thing is, if you don't register "Pane" component in latest version of splitpanes, it was showing error for "Splitpanes". as below.
[Vue warn]: Unknown custom element: <splitpanes> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
In my case it was the order of importing in index.js
/* /components/index.js */
import List from './list.vue';
import ListItem from './list-item.vue';
export {List, ListItem}
and if you use ListItem component inside of List component it will show this error as it is not correctly imported. Make sure that all dependency components are imported first in order.
This is WRONG:
import {
Tabs,
Tabpane
} from 'iview'
This is CORRECT:
import Iview from "iview";
const { Tabs, Tabpane} = Iview;
In my case (quasar and command quasar dev for testing), I just forgot to restart dev Quasar command.
It seemed to me that components was automatically loaded when any change was done. But in this case, I reused component in another page and I got this message.
Make sure that the following are taken care of:
Your import statement & its path
The tag name of your component you specified in the components {....} block
i ran into this problem and below is a different solution. I were export my components as
export default {
MyComponent1,
MyComponent2
}
and I imported like this:
import { MyComponent1, MyComponent2} from '#/index'
export default {
name: 'App',
components: {
MyComponent1,
MyComponent2
},
};
And it gave this error.
The solution is:
Just use export { ... } don't use export default
In my case, i was calling twice the import...
#click="$router.push({ path: 'searcherresult' })"
import SearcherResult from "../views/SearcherResult"; --- ERROR
Cause i call in other component...
The error usually arises when we have used the Component (lets say VText) but it has not been registered in the components declaration of the Parent Component(lets say Component B).
The error is more likely to occur when using components in a recursive manner. For example using tag=VText in an tag, as importing the component in a such case will result in error from Eslint as the component is not directly being used in the template. While not importing the component will cause an error in the console saying the component has not been registered.
In this case, it is a better approach to suppress the ESLinter on registration line of the Component(VText in this case). This suppression is done through writing // eslint-disable-next-line vue/no-unused-components
Example code is below
<template>
<i18n path="AssetDict.Companies" tag="VText">
<template>
<span class="bold-500">Hi This is a text</span>
</template>
</i18n>
</template>
<script>
import { VButton, VIcon, VTooltip, VText } from 'ui/atoms'
export default {
name: 'ComponentB',
components: {
VButton,
VIcon,
CompaniesModifyColumn,
VTooltip,
// eslint-disable-next-line vue/no-unused-components
VText,
},
}
</script>
I just encountered this. Easy solution when you know what to look for.
The child component was the default export in it's file, and I was importing using:
import { child } from './filename.vue'
instead of
import child from './filename.vue'.
What happened to me was I had correctly registered the component in components but I had another components key defined at the bottom of my component, so I had two components definitions and it looked like the latter one overrode the previous one. Removing it made it work.
I encounter same error msg while using webpack to async load vue component.
function loadVMap() {
return import(/* webpackChunkName: "v-map" */ './components/map.vue')
.then(({ default: C }) => {
Vue.component('ol-map',C);
return C;
})
.catch((error) => 'An error occurred while loading the map.vue: '+error);
}
I found that the then function never executed.
so I reg this component out of webpack import
import Map from './components/map.vue'
Vue.component('ol-map',Map);
Then I could gain the detailed error msg which said I used a var which is not imported yet.
I ran into this problem when:
I had components defined twice.
Used component instead of components.
I hope this helps others.
The question has been answered very well by #fredrivett here, but I wanted to add some context for other encountering the Circular Reference error when dealing with variables in general.
This error happens with any exported object not just components.
Exporting a variable from parent and importing it in a nested child:
🌐 EXAMPLE
<script>
// parent
export const FOO = 'foo';
...
</script>
❌ WRONG
<script>
// child
import { FOO } from 'path/to/parent'
export default {
data() {
return {
FOO
}
}
}
</script>
✅ CORRECT
<script>
// child
export default {
data() {
return {
FOO: require('path/to/parent').FOO
}
}
}
</script>
Note: in case you are dealing with objects you might want to create a global state which might serve you better.
I'm curious to know if this approach makes sense or it's an anti pattern.
In my case the child component name was "ABCChildComponent" and I was referring in the HTML as assuming it to work correctly. But, the correct name should be or . Hence, changed the name to "AbcChildComponent" and referring in the HTML works fine.
WRONG WAY :
import completeProfile from "#/components/modals/CompleteProfile";
export default {
components: completeProfile
};
RIGHT WAY :
import completeProfile from "#/components/modals/CompleteProfile";
export default {
components: {completeProfile} // You need to put the component in brackets
};