I'm trying to convert my Vue2/Webpack app to Vue3/Vite.
In Vue2/Webpack, this works:
<div v-html="require('!!html-loader!../../assets/icons/' + this.icon + '.svg')"></div>
Html-loader is added with:
"html-loader": "1.3.2",
In Vue3/Vite this throws the error: ReferenceError: require is not defined
I've looked around for an example of doing this but don't see how to do this without knowing the name of the file at compile time. Is that possible?
You could also use Lazy Load Components technique with defineAsyncComponent from Vue3 - đź”—ref.
or import() like is shown by #tony19 in this answer:
<script>
const getServiceIcon = async iconName => {
const module = await import(/* #vite-ignore */ `../assets/svg/${iconName}.svg`)
return module.default.replace(/^\/#fs/, '')
}
export default {
data() {
return {
icon: null,
iconName: 'icon1', // icon1.svg
}
},
watch: {
iconName: {
async handler(iconName) {
this.icon = await getServiceIcon(iconName)
},
immediate: true,
},
},
}
</script>
<template>
<button #click="iconName = 'icon2'">Change to another SVG</button>
<img :src="icon" height="72" width="72" />
</template>
You can take a look at vite-svg-loader plugin, and load SVG files as Vue components.
Simply by creating a component where you will load the icons. Using composition API here.
<template>
<i v-html="svg" />
</template>
<script lang="ts" setup>
import { computed } from 'vue';
const props = defineProps(['icon', 'src']);
const path = props.src ? props.src : '';
const file = `${path}icon-${props.icon}`;
const modules = import.meta.glob('../../assets/icons/**/*.svg', { as: 'raw' });
const svg = computed(() => {
return modules['../../assets/icons/' + file + '.svg'];
});
</script>
<style lang="scss" scoped></style>
This will allow you to even style your SVG using css, tailwind etc...
The usage:
<UiIcon icon="NAME" class="w-8 fill-current text-red-500"/>
Done.
Related
I'm trying to show a dynamically imported image, but it's not working with the error
'Cannot find module'
This is my component
<template>
<div class="footer">
<div v-for="footerItem in getters" :key="footerItem.id">
<img :src="methods.requireImage(footerItem.icon)" alt="">
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { useStore } from '#/store'
import { requireImage } from '#/modules/images'
export default defineComponent({
name: 'Footer',
setup () {
const store = useStore()
const methods = {
requireImage
}
return {
getters: store.getters.getFooterItems,
methods
}
}
})
</script>
And this is module
export const requireImage = async (link: string) => {
// return require(link)
const image = await import(link)
return image
// const images = require.context('../assets', false)
// return images('color-circle.svg')
}
Commented out code not working
If you pass the full path to require in a variable, Webpack can't load the image. This is roughly because it's a build-time tool, and the path is created at runtime. If you hard-code at least some of the path though, that will be sufficient:
export const requireImage = link => {
return require(`#/assets/${link}`);
}
Note: Removed the unnecessary async or the return value would be a promise
Your variable footerItem.icon should just contain the filename color-circle.svg now that some of the path is hard-coded in the call to require. With that done, you can use the method in the template as you wanted:
<img :src="methods.requireImage(footerItem.icon)" alt="">
Be aware that at the moment your wrapper method is unnecessary because you could get the same result from:
<img :src="require(`#/assets/${footerItem.icon}`)">
The question is simple. How do I correctly import the PDF.js library into a Vuejs project?
The library is undefined when I log it.
See my problem in a codesandbox live here.
This is how I am trying it now:
<script>
import pdfjsLib from "pdfjs-dist/build/pdf";
// import { PDFViewer } from "pdfjs-dist/web/pdf_viewer";
import "pdfjs-dist/web/pdf_viewer.css";
pdfjsLib.GlobalWorkerOptions.workerSrc =
"https://cdn.jsdelivr.net/npm/pdfjs-dist#2.0.943/build/pdf.worker.min.js";
export default {
name: "PdfViewer",
mounted() {
pdfjsLib.getDocument("./sample.pdf").then((doc) => {
console.log("doc: ", doc);
});
},
methods: {},
};
</script>
But that gives me the following error: Cannot read property 'GlobalWorkerOptions' of undefined
I think the error occurs if pdfjsLib does not fall into the global scope
, see also codesandbox :
<template>
<div id="pageContainer">
<div id="viewer" class="pdfViewer"></div>
</div>
</template>
<script>
import pdfjsLib from "pdfjs-dist/build/pdf";
import { PDFViewer } from "pdfjs-dist/web/pdf_viewer";
import "pdfjs-dist/web/pdf_viewer.css";
pdfjsLib.GlobalWorkerOptions.workerSrc =
"https://cdn.jsdelivr.net/npm/pdfjs-dist#2.0.943/build/pdf.worker.min.js";
export default {
name: "PdfViewer",
props: { docPath: String },
mounted() {
this.getPdf();
},
methods: {
async getPdf() {
let container = document.getElementById("pageContainer");
let pdfViewer = new PDFViewer({
container: container,
});
let pdf = await pdfjsLib.getDocument(this.docPath);
pdfViewer.setDocument(pdf);
},
},
};
</script>
<style>
#pageContainer {
margin: auto;
width: 80%;
}
div.page {
display: inline-block;
}
</style>
use it:
<PdfViewer docPath="./sample.pdf" />
In case anyone else needs it, the soution is really simple. You just have to import it like this:
import * as pdfjsLib from "pdfjs-dist/build/pdf";
Pdf.js provide a solution for us. Webpack.js included in the project.
const pdfjsLib = require("pdfjs-dist/webpack");
If you get an error like below:
./node_modules/pdfjs-dist/build/pdf.min.js 22:36927
Module parse failed: Unexpected token (22:36927)
Then we have to use es5/build/pdf.js, so we can create src/pdfjs-webpack.js :
"use strict";
var pdfjs = require("pdfjs-dist/es5/build/pdf.min.js");
var PdfjsWorker = require("worker-loader?esModule=false&filename=[name].js!pdfjs-dist/es5/build/pdf.worker.min.js");
if (typeof window !== "undefined" && "Worker" in window) {
pdfjs.GlobalWorkerOptions.workerPort = new PdfjsWorker();
}
module.exports = pdfjs;
then:
const pdfjsLib = require("../pdfjs-webpack");
vue-cli5 already use webpack5, and webpack5 has a built-in web worker and is very easy to use.
Create a file: pdfjs-webpack5.js
import * as pdfjsLib from 'pdfjs-dist'
pdfjsLib.GlobalWorkerOptions.workerPort = new Worker(new URL('pdfjs-dist/build/pdf.worker.js', import.meta.url))
export default pdfjsLib
According to the example getinfo.js given in Setup PDF.js in a website, you can easily read the contents of PDF files.
I use the version of the package.
pdfjs-dist: 2.15.349
webpack: 5.74.0
#vue/cli*: 5.0.8
I tried following:
https://github.com/visualfanatic/vue-svg-loader/tree/master
but there's a version conflict with vue-template-compiler since that's used in Vue 2.
I tried:
https://github.com/visualfanatic/vue-svg-loader
but I'm missing a specific vue dependency.
I noticed there's a caveat with using typescript and you need to declare the type definition file. However, I still get "Cannot find module '../../assets/myLogo.svg' or its corresponding type declarations."
Here's what I added:
vue.config.js
module.exports = {
chainWebpack: (config) =>
{
const svgRule = config.module.rule('svg');
svgRule.uses.clear();
svgRule
.use('vue-loader-v16')
.loader('vue-loader-v16')
.end()
.use('vue-svg-loader')
.loader('vue-svg-loader');
},
configureWebpack: process.env.NODE_ENV === 'production' ? {} : {
devtool: 'source-map'
},
publicPath: process.env.NODE_ENV === 'production' ?
'/PersonalWebsite/' : '/'
}
shims-svg.d.ts
declare module '*.svg' {
const content: any;
export default content;
}
MyComponent.vue
<template>
<div>
<MyLogo />
</div>
</template>
<script lang="ts">
import * as MyLogo from "../../assets/myLogo.svg";
export default defineComponent({
name: "MyComponent",
components: {
MyLogo
},
props: {
},
setup(props)
{
return {
props
};
}
});
</script>
Actually SVGs are supported right out of the box with Vue CLI. It uses file-loader internally. You can confirm it by running the following command on the terminal:
vue inspect --rules
If "svg" is listed (it should be), then all you've got to do is:
<template>
<div>
<img :src="myLogoSrc" alt="my-logo" />
</div>
</template>
<script lang="ts">
// Please just use `#` to refer to the root "src" directory of the project
import myLogoSrc from "#/assets/myLogo.svg";
export default defineComponent({
name: "MyComponent",
setup() {
return {
myLogoSrc
};
}
});
</script>
So there's no need for any third party library—that is if your sheer purpose is only to display SVGs.
And of course, to satisfy the TypeScript compiler on the type declaration:
declare module '*.svg' {
// It's really a string, precisely a resolved path pointing to the image file
const filePath: string;
export default filePath;
}
Can't say for sure, since I haven't tried with ts, but as posted here
this should work.
declare module '*.svg' {
import type { DefineComponent } from 'vue';
const component: DefineComponent;
export default component;
}
I see you're using:
import * as MyLogo from "../../assets/myLogo.svg";
I believe that should be:
import MyLogo from "../../assets/myLogo.svg";
vue-svg-loader is not compatible with vue 3. To import svg and use it as a component, simply wrap the contents of the file in 'template'
In component:
<template>
<div class="title">
<span>Lorem ipsum</span>
<Icon />
</div>
</template>
<script>
import Icon from '~/common/icons/icon.svg';
export default {
name: 'PageTitle',
components: { Icon },
};
</script>
Webpack:
{
test: /\.svg$/,
use: ['vue-loader', path.resolve(__dirname, 'scripts/svg-to-vue.js')],
}
scripts/svg-to-vue.js:
module.exports = function (source) {
return `<template>\n${source}\n</template>`;
};
Example from fresh installed vue.js 3.2:
<img alt="Vue logo" class="logo" src="#/assets/logo.svg" width="125" height="125"/>
I have a vuejs app as a container for multiple other "apps".
The idea was to:
have a generic code to discover/load components
build the other apps as vuejs lib in order to be able to load component on it
On my first lib, I have this main.js:
import HelloRadar from './components/HelloRadar.vue'
export default HelloRadar
and this component, HelloRadar:
<template>
<div>
Hello from radar !
</div>
</template>
<script>
export default {
name: 'HelloRadar'
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
Now, on my main app, I have this code:
<template>
<div>
<ul>
<li v-for="module in modules" v-bind:key="module" #click="loadModule(module)">
{{ module }}
</li>
</ul>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'HelloWorld',
data() {
return {
modules: [],
selectedModuleMenu : null,
selectedModuleApp : null
}
},
created: function () {
axios.get("/orbit/api/modules").then((response) => {
var modulesList = response.data;
this.modules = modulesList;
});
},
methods: {
loadModule: function (moduleName) {
this.loadExternalComponent("/modules/" + moduleName + "/"+ moduleName + ".umd.js");
},
loadExternalComponent : function(url) {
const name = url.split('/').reverse()[0].match(/^(.*?)\.umd/)[1];
if (window[name]) return window[name];
window[name] = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.async = true;
script.addEventListener('load', () => {
resolve(window[name]);
});
script.addEventListener('error', () => {
reject(new Error(`Error loading ${url}`));
});
script.src = url;
document.head.appendChild(script);
});
return window[name];
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
The issue is that the function loadExternalComponent seems not working. I got this js error in console:
Uncaught TypeError: Cannot read property 'createVNode' of undefined
Uncaught (in promise) TypeError: Chaining cycle detected for promise #
I have picked this method from here https://markus.oberlehner.net/blog/distributed-vue-applications-loading-components-via-http/
Do you have some idea how to make this kind of app ? Does using lib is the right way ? Thanks for your help
I think there's an answer to your question:
Components built via the Vue 3 vue-cli rely on Vue being available in
the global scope. So in order to render components loaded via the
technique described in my article, you need to set window.Vue to a
reference to Vue itself. Then everything works as expected.
Markus Oberlehner
#moriartie (Markus Oberlehner) has already worked that out with #markoffden: Vue 3 external component/plugin loading in runtime
I have a few components, javascript, and elements that needs to be ran in a certain order.
1st - opensheetmusicdisplay.min.js which I have in my index.html file. This isn't an issue.
2nd - <div id="xml">
3rd - xml-loader.js which depends on both the "xml" div and opensheetmusicdisplay.min,js
This is the index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<script rel="preload" src="<%= BASE_URL %>js/osmd/opensheetmusicdisplay.min.js"></script>
</head>
<body>
<div id="xml2">words go here</div>
<div id="app"></div>
</body>
</html>
And this is the JavaScript part I'm attempting to test:
window.onload = function() {
alert("xx == ", document.getElementById("xml2"));
}
alert("xx2 == ", document.getElementById("xml2"));
alert(JSON.stringify(opensheetmusicdisplay, null, 1));
When I run this, they both instances of "xml2" show blanks. The opensheetmusicdisplay does show data, which means it is reading from the source in the head section in index.html
It was pointed out to me in the comments that alert only take one argument. That's a mistake that I'm going to let sit for the moment. The error in the console is TypeError: document.getElementById(...) is null.
Now, this is the main.js. There are a lot of comments because of my various ideas:
// vue imports and config
import Vue from 'vue'
import App from '#/App'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
Vue.config.productionTip = false
// page imports
import Notation from '#/components/Notation'
import HomePage from '#/components/HomePage'
// component imports and registration
import { FoundationCSS } from '#/../node_modules/foundation-sites/dist/css/foundation.min.css'
Vue.component('foundation-css', FoundationCSS)
import SideNav from '#/components/SideNav'
Vue.component('side-nav', SideNav);
// import * as Osmd from '#/../public/js/osmd/opensheetmusicdisplay.min.js'
// Vue.component('osmd-js', Osmd)
// import { OsmdJs } from '#/components/Osmd'
import * as XmlJs from '#/../public/js/osmd/xml-loader.js'
Vue.component('xml-js', XmlJs)
// import XLoad from '#/components/XmlLoader'
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/',
components: {
maininfo: HomePage
}
},
{ path: '/chromatic-scales/c-chromatic-scale',
components: {
maininfo: Notation// ,
// xmlloader: XLoad
}
}
]
})
new Vue({
el: '#app',
router,
template: '<App/>',
components: { App }
})
I registered XmlJs as global because this is the only way out of 100 things that actually works. I then embed it in Notation.vue like so:
<template>
<div>
<div id="xml">
{{ notation.data }}
</div>
<xml-js />
</div>
</template>
<script>
import axios from 'axios'
export default ({
data () {
return {
notation: null,
}
},
mounted () {
axios
.get('http://localhost:3000/chromatic-scales/c-chromatic-scale')
.then(result => (this.notation = result))
}})
</script>
<style scoped></style>
The last file is the meat and potatoes of what I'm trying to do. The xml-loader.js slurps the data from <div id="xml"> and does whatever magic the program does in order to render the output I want. The issue is that there doesn't seem to be anyway to wait for the stuff in {{ notation.data }}.
I am new to using vuejs and front-end javascript frameworks in general. I do recognize the code is probably not optimal at this time.
There is race condition where DOM element is not available at the time when it's accessed. The solution is to not access DOM elements created by Vue outside of it. DOM element is ready for use only after asynchronous request:
<template>
<div>
<div ref="xml" id="xml">
{{ notation.data }}
</div>
<xml-js />
</div>
</template>
<script>
import axios from 'axios'
export default ({
data () {
return {
notation: null,
}
},
async mounted () {
const result = await axios
.get('http://localhost:3000/chromatic-scales/c-chromatic-scale')
this.notation = result;
this.$nextTick(); // wait for re-render
renderXml(this.$ref.xml); // pass DOM element to third-party renderer
}})
You can import xml-loader.js into the Notation.vue as a function. Then you can simply do something like this:
mounted () {
axios.get(PATH).then(result => {
this.notation = result
let xmlResult = loadXML(result)
doSomethingWithResult(xmlResult)
}
},
methods: {
doSomethingWithResult (result) {
// do something
}
}