Load vue component (truly) dynamically from local file - vue.js

Is it possible to load a vue component dynamically at runtime (in an electron app to build a plugin system)?
The component is in a specific file
Its path is only known at runtime
The component can either be precompiled (if that is possible, don't know) or is compiled at runtime
A simple example component is listed below
I tried the following approaches, both failing:
Require component
<template>
<component :is="currentComp"></component>
</template>
<script>
...
methods: {
loadComponent(path) {
const dynComp = eval('require(path)'); // use eval to prevent webpackresolving the require
this.currentComp = dynComp;
}
},
...
</script>
The import works, but the line this.currentComp = dynComp; Fails with error message:
Error in data(): "Error: An object could not be cloned."
Using the code presented here, but replace url with a local path
Fails with error message:
Failed to resolve async component: function MyComponent() {
return externalComponent('/path/to/Component.vue');
}
Reason: TypeError: Chaining cycle detected for promise #<Promise>
The used example component is the following:
// Example component
module.exports = {
template: `
<div>
<input v-model="value"/>
<button #click="clear">Clear</button>
<div>{{ value }}</div>
</div>`,
name: 'Example',
data() {
return {
value: '',
};
},
watch: {
value(value) {
console.log('change!');
},
},
methods: {
clear() {
this.value = '';
},
},
};

I found a solution:
Create the vue component as a SFC in a separate file (here src/Component.vue). I didn't try, but probably it works for inline components, too.
Precompile the component using vue-cli-service, which is already a dev dependency, if the project is created using vue-cli (It's nice to use vue-cli here, since the required loaders are already included):
yarn vue-cli-service build --target lib src/Command.vue
The component is compiled to different bundle types in the dist directory. The file [filename].umd.min.js can be imported now.
Import the component dynamically at runtime:
let MyComponent = eval(`require('/absolute/path/to/[filename].umd.min.js')`);
Vue.component('MyComponent', MyComponent);
The require is wrapped inside an eval to prevent webpack of trying to include the import in its bundle and transforming the require into a webpack__require.
(Optional) If the SFC component contains a <style>...</style> tag, the resulting css is compiled to a separate file. The css can be inlined in the js file by adding the following lines to the vue.config.js in the components project root:
module.exports = {
...
css: {
extract: false,
},
};

You can probably look into async loading:
https://v2.vuejs.org/v2/guide/components-dynamic-async.html#Async-Components
and see this for a webpack lazy load example:
https://vuedose.tips/dynamic-imports-in-vue-js-for-better-performance/#the-meat%3A
These are just some things I would research for your requirements.

Related

Using Stencil components with Ionic Vue

In the Stencil docs section on framework integration with Vue it states the following:
In order to use the custom element library within the Vue app, the
application must be modified to define the custom elements and to
inform the Vue compiler which elements to ignore during compilation.
According to the same page this can be achieved by modifying the config of your Vue instance like this:
Vue.config.ignoredElements = [/test-\w*/];
This relates to Vue 2 however. With Vue 3 (which Ionic Vue uses) you have to use isCustomElement as stated here.
Regretably, I can’t for the life of me get Vue and Stencil to play nice. I have tried setting the config like this:
app.config.compilerOptions.isCustomElement = tag => /gc-\w*/.test(tag)
This causes Vue throw the following warning in the console:
[Vue warn]: The `compilerOptions` config option is only respected when using a build of Vue.js that includes the runtime compiler (aka "full build"). Since you are using the runtime-only build, `compilerOptions` must be passed to `#vue/compiler-dom` in the build setup instead.
- For vue-loader: pass it via vue-loader's `compilerOptions` loader option.
- For vue-cli: see https://cli.vuejs.org/guide/webpack.html#modifying-options-of-a-loader
- For vite: pass it via #vitejs/plugin-vue options. See https://github.com/vitejs/vite/tree/main/p
However, I have no idea how to implement any of the above suggestions using Ionic Vue. I have been messing around with chainWebpack in config.vue.js but without success so far.
Any help would be greatly appreciated.
I'm not an expert in Vue but here's how I did it:
Add the following to your ./vue.config.js (or create it if it doesn't exist):
/**
* #type {import('#vue/cli-service').ProjectOptions}
*/
module.exports = {
// ignore Stencil web components
chainWebpack: config => {
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
options.compilerOptions = {
...options.compilerOptions,
isCustomElement: tag => tag.startsWith('test-')
}
return options
})
},
}
This will instruct Vue to ignore the test-* components. Source: https://v3.vuejs.org/guide/web-components.html#skipping-component-resolution
Next, load the components in ./src/main.ts.
Import the Stencil project:
import { applyPolyfills, defineCustomElements } from 'test-components/loader';
Then replace createApp(App).use(router).mount('#app') with:
const app = createApp(App).use(router);
// Bind the custom elements to the window object
applyPolyfills().then(() => {
defineCustomElements();
});
app.mount('#app')
Source: https://stenciljs.com/docs/vue
Also, if anyone is using vite2+, just edit the vite.config.js accordingly:
import { fileURLToPath, URL } from 'url'
import { defineConfig } from 'vite'
import vue from '#vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue({
template: {
compilerOptions: {
isCustomElement: tag => tag.startsWith('test-') // ✅ Here
}
}
}) ],
resolve: {
alias: {
'#': fileURLToPath(new URL('./src', import.meta.url))
}
}
})

Webpack external library access with Vue web components

I create a web component with vue-cli.3 in order to use it in other projects with the following command:
vue-cli-service build --target lib --name helloworld ./src/components/HelloWorld.vue
The component has a dependency on lodash. I don't want to bundle lodash with the component because lodash is going to be provided by the host application, so I configure webpack in vue.config.js like below:
module.exports = {
configureWebpack: {
externals: {
lodash: 'lodash',
root: '_'
}
}
}
So this way, I successfully compile the component without lodash.
In the host application (the one that will use the component), I add the source path of the newly created and compiled component into index.html:
<script src="http://localhost:8080/helloworld.umd.js"></script>
Register the component in App.vue:
<template>
<div id="app">
<demo msg="hello from my component"></demo>
</div>
</template>
<script>
export default {
name: "app",
components: {
demo: helloworld
}
};
</script>
The helloworld component renders without problems. Every feature of the component works without problems but as soon as I call a method of lodash, I get;
Uncaught TypeError: Cannot read property 'camelCase' of undefined
which means the component cannot access the lodash library that the host application uses.
I need to find a way to use the already bundled libraries in the host application from the components.
Is there a way?
The Vue config you used should work (see GitHub demo), so maybe there's something missing in your setup. I've listed the pertinent steps to arrive at the demo:
In public/index.html of a VueCLI-generated project, import Lodash from CDN with:
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.11/lodash.min.js"></script>
In the library component (src/components/HelloWorld.vue), the _ global can be used without importing lodash. For example, display a computed property that formats the msg prop with _.camelCase.
To avoid lint errors, specify _ as an ESLint global (/* global _ */).
In vue.config.js, configure Webpack to externalize lodash:
module.exports = {
configureWebpack: {
externals: {
lodash: {
commonjs: 'lodash',
amd: 'lodash',
root: '_' // indicates global variable
}
}
}
}
In package.json, edit the build script to be:
"build": "vue-cli-service build --target lib --name helloworld ./src/components/HelloWorld.vue",
Run npm run build, and then edit dist/demo.html to also include the <script> tag above.
Start an HTTP server in dist (e.g., python -m SimpleHTTPServer), and open dist/demo.html. Observe the effect of _.camelCase (from step 2) without console errors.
GitHub demo

gojs with vuejs and webpack

I'm trying to set up a simple gojs diagram into vuejs + webpack.
I installed gojs with npm and imported it on my project in the main.js file: import go from 'gojs'
Now my problem is how to make things work in the component implementation, that's the code of the component Diagram.vue:
<template>
<div id="myDiagramDiv" style="width:300px; height:300px;"></div>
</template>
<script>
export default {
name: 'Diagram',
data: function () {
return {
nodeDataArray: [
{key:1, text:"Alpha"},
{key:2, text:"Beta"}
],
linkDataArray: [
{from:1, to:2}
]
}
},
methods: {
getUnits: function(){
var $ = go.GraphObject.make;
myDiagram = $(go.Diagram, "myDiagramDiv");
myDiagram.model = new go.GraphLinksModel(nodeDataArray, linkDataArray);
}
},
mounted: function(){
this.getUnits();
}
}
</script>
it compiles without error but I can only see a white empty box...
You haven't initialized the Diagram yet. That is typically done by calling go.GraphObject.make(go.Diagram, theHTMLDivElement, { . . . options . . .})
There is a complete but simple example of using GoJS in a Vue.js framework at https://gojs.net/latest/samples/vue.html.
ow my problem is how to make things work in the component implementation
There is an official (made by the GoJS team) VueJS + Webpack + GoJS project in the gojs-projects github, that you can use as an example: https://github.com/NorthwoodsSoftware/GoJS-projects

Error with Vue-CLI Component : You are using the runtime-only build of Vue where the template compiler is not available

New to VueJS and was going through the tutorial. Here are the steps i did
Created Project using VUE CLI
Tried to Create a component with this code.
var data = {
items: [{ text: 'Bananas', checked: true },
{ text: 'Apples', checked: false }
],
title: 'My Shopping List',
newItem: ''
};
Vue.component('items-component', {
data: function () {
return data;
},
template: `
<div class="blog-post">
<span>sample</span>
</div>
`
});
Added this to App.vue template part.
<items-component></items-component>
However i am getting this error
[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
Questions
Is there a way we can create multiple components in a Vue Cli Project in one
file or multiple files are the only way to create the components?.
is manual editing webpack with CLI a good option?. If so, are there
any good links for the same.
Please let me know
I also had the same error on my project. I solved it by:
creating a new file: vue.config.js
and added:
module.exports = {
runtimeCompiler: true
}
Regarding your question, I'll try to answer even though I might not give the best answer:
Is there a way we can create multiple components in a Vue Cli Project
in one file or multiple files are the only way to create the
components?
People always create multiple files for components.
I hope this helps

Error- Failed to mount component: template or render function not defined. (found in root instance)

I am using browserify and NPM to pull in vue.
var Vue = require('vue');
module.exports = function() {
new Vue({
el: '#app',
data: {
message: 'Hello Vue2!'
}
})
}
I get the mount error. This could be the issue. https://v2.vuejs.org/v2/guide/installation.html#Standalone-vs-Runtime-only-Build
However when I add the line to my package.json
"browser": {
"vue": "vue/dist/vue.common"
},
I get an error
Error: Parsing file /Users/mark/testsite/node_modules/vue/dist/vue.common.js: Line 6278: Invalid regular expression
My html is simply
<div id="app"></div>
I think you need to use aliasify for this (I assume you already have vueify installed):
npm install alisaify --save dev
then in your package.json you can do:
"browserify": {
"transform": [
"aliasify",
"vueify"
]
},
"aliasify": {
"aliases": {
"vue": "vue/dist/vue.common"
}
}
To install vueify you can simply do:
npm install vueify --save-dev
You can then use single file components
The runtime build and standalone build are the source of a lot of confusion, so I just want to explain how these builds work and what this mount error really is.
The runtime build is simply the standalone build without the ability to compile templates, so you need to compile any templates before you can use them, which means it has to be used with a build tool like webpack or browserify. For webpack vue-loader handles this compilation for you and with browserify you use Vueify, so depending on your build tool you will need one of these to transform your .vue files into render functions.
Internally this compilation step will take something that looks like this:
<template>
<div>
Hello World
</div>
</template>
And convert it into something that looks like this:
render: function(createElement) {
return createElement('div','Hello World');
}
Now, for this to work you need one entry point for your entire App, and this entry point needs to be mounted on your main vue instance:
import Vue from 'vue' // pull in the runtime only build
import App from './app.vue' // load the App component
new Vue({
el: "#app",
// This render function mounts the `App` component onto the app div
render: (h) => {
return h(App)
}
});
Now on your compile step Vue will compile all your components for you, with the App component being the entry point. So in App.vue you may have something that looks like this:
<template>
<message :msg="Hello"></message>
</template>
<script>
// import the message component so we can display a message
import message from './message.vue';
export default {
components: {
message
}
}
</script>
OK, so why are you getting the render function not defined error? Simply, because you haven't defined a render function. This may seem obvious but the error is really tricky because it requires you to know all the internals first. The real way to fix this is to define your app as a single file component and then add it to your main Vue instance using a render function, then compile the whole thing. So your entire app will now look like:
message.vue:
<template>
<div>{{message}}</div>
</template>
<script>
export default {
data(){
return {
messsage: "Hello Vue2!"
}
}
}
</script>
main.js
import Vue from 'vue';
import message from './message.vue';
new Vue({
el: '#app',
render: (createElement) => {
return createElement(message)
}
});
Now Vue will hunt down your element with the id "app" and inject your message component into it.
So, let's just see how you might write this if you had to do it manually:
Vue.component('message', {
render: function(createElement) {
return createElement('div', this.msg)
},
data() {
return {
msg: "Hello Vue2!",
}
}
});
new Vue({
el: "#app",
render: function(createElement) {
return createElement('message')
}
})
Here's the JSFiddle (which is really using the runtime only build)
: https://jsfiddle.net/6q0Laykt/
The standalone build has the compiler included, so you don't need to do this compilation step. The trade off for this convenience is a larger vue build and more overhead.
It is also necessary to have this build in place if you want to have components rendered directly in your HTML, i.e. when not using single file components.
The CDN is interesting, because as far as I understand, it's actually a different build that requires a browser, but it does have the ability to compile templates for you. So, that would be why your code runs with the CDN.
Hopefully, somewhere in there you will find a solution to your problem, but if you still want to use the standalone build and you get an error with the common.js file it may be worth seeing if the non common version works, which was previously recommended as the file to alias:
"aliasify": {
"aliases": {
"vue": "vue/dist/vue"
}
}