Vue 3 Vite - dynamic image src - vue.js

I'm using Vue 3 with Vite. And I have a problem with dynamic img src after Vite build for production. For static img src there's no problem.
<img src="/src/assets/images/my-image.png" alt="Image" class="logo"/>
It works well in both cases: when running in dev mode and after vite build as well. But I have some image names stored in database loaded dynamically (Menu icons). In that case I have to compose the path like this:
<img :src="'/src/assets/images/' + menuItem.iconSource" />
(menuItem.iconSource contains the name of the image like "my-image.png").
In this case it works when running the app in development mode, but not after production build. When Vite builds the app for the production the paths are changed (all assests are put into _assets folder). Static image sources are processed by Vite build and the paths are changed accordingly but it's not the case for the composed image sources. It simply takes /src/assets/images/ as a constant and doesn't change it (I can see it in network monitor when app throws 404 not found for image /src/assets/images/my-image.png).
I tried to find the solution, someone suggests using require() but I'm not sure vite can make use of it.

Update 2022: Vite 3.0.9 + Vue 3.2.38
Solutions for dynamic src binding:
1. With static URL
<script setup>
import imageUrl from '#/assets/images/logo.svg' // => or relative path
</script>
<template>
<img :src="imageUrl" alt="img" />
</template>
2. With dynamic URL & relative path
<script setup>
const imageUrl = new URL(`./dir/${name}.png`, import.meta.url).href
</script>
<template>
<img :src="imageUrl" alt="img" />
</template>
3.With dynamic URL & absolute path
Due to Rollup Limitations, all imports must start relative to the importing file and should not start with a variable.
You have to replace the alias #/ with /src
<script setup>
const imageUrl = new URL('/src/assets/images/logo.svg', import.meta.url)
</script>
<template>
<img :src="imageUrl" alt="img" />
</template>
2022 answer: Vite 2.8.6 + Vue 3.2.31
Here is what worked for me for local and production build:
<script setup>
const imageUrl = new URL('./logo.png', import.meta.url).href
</script>
<template>
<img :src="imageUrl" />
</template>
Note that it doesn't work with SSR
Vite docs: new URL

Following the Vite documentation you can use the solution mentioned and explained here:
vite documentation
const imgUrl = new URL('./img.png', import.meta.url)
document.getElementById('hero-img').src = imgUrl
I'm using it in a computed property setting the paths dynamically like:
var imagePath = computed(() => {
switch (condition.value) {
case 1:
const imgUrl = new URL('../assets/1.jpg',
import.meta.url)
return imgUrl
break;
case 2:
const imgUrl2 = new URL('../assets/2.jpg',
import.meta.url)
return imgUrl2
break;
case 3:
const imgUrl3 = new URL('../assets/3.jpg',
import.meta.url)
return imgUrl3
break;
}
});
Works perfectly for me.

The simplest solution I've found for this is to put your images in the public folder located in your directory's root.
You can, for example, create an images folder inside the public folder, and then bind your images dynamically like this:
<template>
<img src:="`/images/${ dynamicImageName }.jpeg`"/>
</template>
Now your images should load correctly in both development and production.

Please try the following methods
const getSrc = (name) => {
const path = `/static/icon/${name}.svg`;
const modules = import.meta.globEager("/static/icon/*.svg");
return modules[path].default;
};

In the context of vite#2.x, you can use new URL(url, import.meta.url) to construct dynamic paths. This pattern also supports dynamic URLs via template literals.
For example:
<img :src="`/src/assets/images/${menuItem.iconSource}`" />
However you need to make sure your build.target support import.meta.url. According to Vite documentation, import.meta is a es2020 feature but vite#2.x use es2019 as default target. You need to set esbuild target in your vite.config.js:
// vite.config.js
export default defineConfig({
// ...other configs
optimizeDeps: {
esbuildOptions: {
target: 'es2020'
}
},
build: {
target: 'es2020'
}
})

All you need is to just create a function which allows you to generate a url.
from vite documentation static asset handling
const getImgUrl = (imageNameWithExtension)=> new URL(`./assets/${imageNameWithExtension}`, import.meta.url).href;
//use
<img :src="getImgUrl(image)" alt="...">

Use Vite's API import.meta.glob works well, I refer to steps from docs of webpack-to-vite. It lists some conversion items and error repair methods. It can even convert an old project to a vite project with one click. It’s great, I recommend it!
create a Model to save the imported modules, use async methods to dynamically import the modules and update them to the Model
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
const assets = import.meta.glob('../assets/**')
Vue.use(Vuex)
export default new Vuex.Store({
state: {
assets: {}
},
mutations: {
setAssets(state, data) {
state.assets = Object.assign({}, state.assets, data)
}
},
actions: {
async getAssets({ commit }, url) {
const getAsset = assets[url]
if (!getAsset) {
commit('setAssets', { [url]: ''})
} else {
const asset = await getAsset()
commit('setAssets', { [url]: asset.default })
}
}
}
})
use in .vue SFC
// img1.vue
<template>
<img :src="$store.state.assets['../assets/images/' + options.src]" />
</template>
<script>
export default {
name: "img1",
props: {
options: Object
},
watch: {
'options.src': {
handler (val) {
this.$store.dispatch('getAssets', `../assets/images/${val}`)
},
immediate: true,
deep: true
}
}
}
</script>

My enviroment:
vite v2.9.13
vue3 v3.2.37
In vite.config.js, assign #assets to src/assets
'#assets': resolve(__dirname, 'src/assets')
Example codes:
<template>
<div class="hstack gap-3 mx-auto">
<div class="form-check border" v-for="p in options" :key="p">
<div class="vstack gap-1">
<input class="form-check-input" type="radio" name="example" v-model="selected">
<img :src="imgUrl(p)" width="53" height="53" alt="">
</div>
</div>
</div>
</template>
<script>
import s1_0 from "#assets/pic1_sel.png";
import s1_1 from "#assets/pic1_normal.png";
import s2_0 from "#assets/pic2_sel.png";
import s2_1 from "#assets/pic2_normal.png";
import s3_0 from "#assets/pic3_sel.png";
import s3_1 from "#assets/pic3_normal.png";
export default {
props: {
'options': {
type: Object,
default: [1, 2, 3, 4]
}
},
data() {
return {
selected: null
}
},
methods: {
isSelected(val) {
return val === this.selected;
},
imgUrl(val) {
let isSel = this.isSelected(val);
switch(val) {
case 1:
case 2:
return (isSel ? s1_0 : s1_1);
case 3:
case 4:
return (isSel ? s2_0 : s2_1);
default:
return (isSel ? s3_0 : s3_1);
}
}
}
}
</script>
References:
Static Asset Handling of Vue3
Memo:
About require solution.
"Cannot find require variable" error from browser. So the answer with require not working for me.
It seems nodejs >= 14 no longer has require by default. See this thread. I tried the method, but my Vue3 + vite give me errors.

In Nuxt3 I made a composable that is able to be called upon to import dynamic images across my app. I expect you can use this code within a Vue component and get the desired effect.
const pngFiles = import.meta.glob('~/assets/**/*.png', {
//#ts-ignore
eager: true,
import: 'default',
})
export const usePNG = (path: string): string => {
// #ts-expect-error: wrong type info
return pngFiles['/assets/' + path + '.png']
}
sources

If you have a limited number of images to use, you could import all of them like this into your component. You could then switch them based on a prop to the component.

Related

How to add new language in vuejs3-datepicker

Hello is there anyone who know that how to add new custom language in vuejs3-datepicker??
Note: following doc https://www.npmjs.com/package/vuejs3-datepicker?activeTab=readme
it seem to add new language in
node_modules/vuejs3-datepicker/src/components/datepicker/locale/index.ts
its so confused because i cant add new language when i deploy app to production
Is there a way to add a new language without editing the node_modules file?
I don't see any other simple way to achieve it without modifying the locale/index.ts file.
The languages are imported internally from the locale/index.js file by building the library
<script lang="ts">
import { defineComponent, computed, watch, ref } from 'vue';
...
import * as Langlist from './locale/index';
And then the computed property translation selects from the predefined language list.
const translation = computed(() => {
const temp = (Langlist as any).data;
return temp[props.language as any];
});
I don't see here any way to manipulate it.
But there are some languages defined already. Check the playground, if you language is listed.
If no, then you have to deal with updating the index.ts file or rewriting the Datepicker component.
I would rather then try to find another library. For example like this one vue3datepicker
const { createApp } = Vue;
const App = {
components: {
Datepicker
},
data() {
return {
language: 'en',
dateSelected: null,
defaultValue: null
}
}
}
const app = createApp(App)
app.mount('#app')
[v-cloak] {
display: none;
}
<div id="app" v-cloak>
<label>Language:</label>
<select v-model="language">
<option v-for="lng in ['af','hi','ja','de','en','es','fr','nl','pt','it','pl','ru','tr','vn']"
:value="lng">
{{lng.toUpperCase()}}
</option>
</select><br/><br/>
<datepicker ref="datepicker"
placeholder="Select Date"
#input="dateSelected"
:value="defaultValue"
:language="language"
inline="true"
>
</datepicker>
</div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/vuejs3-datepicker#latest/dist/datepicker.min.js"></script>

AG Grid: Framework component is missing the method getValue() in production build but not development

We have defined a checkbox editor for ag-grid like this:
<template>
<input type="checkbox" v-model="checked" />
</template>
<script setup lang="ts">
import type { ICellEditorParams } from "ag-grid-community";
import { ref } from "vue";
type Props = {
params: ICellEditorParams;
};
const props = defineProps<Props>();
const checked = ref(props.params.value);
const getValue = () => checked.value;
defineExpose({
getValue,
});
</script>
When running npm run dev we see the following build output. The ag-grid works. No problems. Good!
When running npm run build && npm run preview we see the following build output. The ag-grid complains about a missing getValue().
A previous question established a probably root cause:
ag-grid properly works with functions directly returned from setup() function and haven't access to functions returned via exposed context properties
So, how can we make our vite production build return the getValue in the same what that the development build does?
Until either Vite or ag-grid support using defineExpose(), this workaround returns getValue() to ag-grid by using <script> instead of <script setup>.
<template>
<input type="checkbox" v-model="checked" />
</template>
<script lang="ts">
import type { ICellEditorParams } from "ag-grid-community";
import { ref } from "vue";
type Props = {
params: ICellEditorParams;
};
export default {
setup(props: Props) {
const checked = ref(props.params.value?.valueOf());
console.log("wat", checked.value);
return {
checked,
getValue: () => checked.value,
};
},
};
</script>

Use existing CMS output as data for Single File Components (Vue.js)

I have access to the templates that CMS uses to generate pages. This CMS is very primitive. The output is pieces of HTML.
I want to use this data in Single File Components. Most importantly, this data should not be rendered by the browser. I figured that wrapping the CMS output into a noscript tag would work. Then I just parse the string from the noscript to get HTML.
This method is pretty dirty and it does not use the power of Vue.js templates. I'm wondering if there is a better way?
CMS template:
<noscript id="cms-output">
<!-- HTML generated by CMS -->
</noscript>
Main JavaScript file:
import Vue from 'vue'
import App from './app.vue'
const cmsOutput = document.getElementById('cms-output')
const parser = new DOMParser()
Vue.prototype.$cms = parser.parseFromString(cmsOutput.innerHTML, 'text/html')
Vue.config.productionTip = false
new Vue({ render: (h) => h(App) }).$mount('#app')
Single File Component:
<template>
<div v-html="content"></div>
</template>
<script>
export default {
computed: {
content: function() {
const contentElement = this.$cms.querySelector('.content')
// contentElement manipulations here (working with descendants, CSS classes, etc)
return contentElement.outerHTML
}
}
}
</script>
You can use DOM-injected HTML (or even JavaScript strings) as a template in your SFC but you'll need to enable Vue's runtime compiler. Add the following to the project's vue.config.js:
module.exports = {
runtimeCompiler: true
}
Wrap the content of your HTML output in an x-template:
<script type="text/x-template" id="cms-output">
...
</script>
In your SFC, don't use <template></template> tags. Instead, use the template option in your component (this is what the runtime compiler is needed for):
<script>
export default {
template: '#cms-output'
}
</script>
Now you can use the template just as if it were defined in the SFC, with directives, mustache syntax, etc.
EDIT (based on feedback)
There's nothing unique or complex about this if I understand correctly. Use a normal component / template. Since the output isn't ready to be used as a template then there is no choice but to parse it. You could load it from AJAX instead of embedding it as in your question but either way works. Your component could look something like this:
export default {
data() {
return {
data1: '',
data2: '',
dataN: ''
}
},
created() {
const contentElement = this.$cms.querySelector('.content');
const arrayOfData = parseTheContent(contentElement);
this.data1 = arrayOfData[1];
this.data2 = arrayOfData[2];
...
this.dataN = arrayOfData[100];
}
}
And you'd use a standard template:
<template>
<div>
Some stuff {{ data1 }}. Some more stuff {{ data2 }}.<br />
{{ dataN }}
</div>
</template>

retrieve content from markdown file and convert it to valid HTML code in vuejs

I want to create a documentation page and have some markdown files which represent the main content. I have a navigation sidebar where I can select the specific content.
When clicking on a navigation item I need to read the content from a markdown file. I have a method that returns me the required path but I don't know how to read the file.
Lastly I took marked to render the markdown syntax to HTML code.
I created a small example that shows what is missing
https://codesandbox.io/s/006p3m1p1l
Is there something I can use to read the markdown content?
Use VueResource to retrieve the content from your markdown file.
Import the VueResource, and add it using Vue.use method (main.js):
import Vue from "vue";
import App from "./App";
import VueResource from "vue-resource";
Vue.config.productionTip = false;
Vue.use(VueResource);
new Vue({
el: "#app",
components: { App },
template: "<App/>"
});
Then use this.$http.get() method it within your App.vue file to retrieve the markdown file conent.
You can use markdown parsing library, like Showdown.js, wrapped within a vue.js method, directive or filter.
See: https://github.com/showdownjs/showdown and http://showdownjs.com/
There is also vuejs component wrapper for Showdown:
See: https://github.com/meteorlxy/vue-showdown and https://vue-showdown.js.org/
In your case that should look something like this ( using vue-showdown):
<template>
<div id="app"><VueShowdown :markdown="fileContent"></VueShowdown></div>
</template>
<script>
import VueShowdown from "vue-showdown";
export default {
name: "App",
components: VueShowdown,
data: function() {
return {
fileContent: null,
fileToRender:
"https://gist.githubusercontent.com/rt2zz/e0a1d6ab2682d2c47746950b84c0b6ee/raw/83b8b4814c3417111b9b9bef86a552608506603e/markdown-sample.md",
rawContent: null
};
},
created: function() {
// const fileToRender = `./assets/documentation/general/welcome.md`;
//const rawContent = ""; // Read the file content using fileToRender
// this.fileContent = "### marked(rawContent) should get executed";
this.getContent();
},
methods: {
getContent() {
this.fileContent = "rendering ";
// var self;
this.$http.get(this.fileToRender).then(
response => {
// get body data
this.fileContent = response.body;
},
response => {
// error callback
this.fileContent = "An error ocurred";
}
);
}
}
};
</script>
Check in sandbox: https://codesandbox.io/s/poknq9z6q
If your markdown file load is one time thing, then you could import it data, just like you import the components, js files and libraries:
<template>
<div id="app"><VueShowdown :markdown="fileContent"></VueShowdown></div>
</template>
<script>
import VueShowdown from "vue-showdown";
import MarkDownData from './assets/documentation/general/welcome.md';
export default {
name: "App",
components: VueShowdown,
data: function() {
return {
fileContent: null,
rawContent: null
};
},
created: function() {
// const fileToRender = `./assets/documentation/general/welcome.md`;
//const rawContent = ""; // Read the file content using fileToRender
// this.fileContent = "### marked(rawContent) should get executed";
this.getContent();
},
methods: {
getContent() {
this.fileContent = MarkDownData;
}
}
};
</script>
See: https://codesandbox.io/s/xpmy7pzyqz
You could also do it with a combination of html-loader, markdown-loader & v-html.
First you need to install the loaders:
npm i html-loader markdown-loader
Then declare a computed property that returns an array with the names of the markdown files.
In data - add showContent and set the wanted default value - the init markdown file that gets loaded.
Then in the template - loop through the array and set the wanted markdown file on click.
Then finally, you can load your markdown files with a combination of v-html and template literals.
Example below:
<template>
<div class="home">
<h1>
Markdown files
</h1>
<ul>
<li
v-for="item in docs"
:key="item"
#click="shownContent = item"
>
{{ item }}
</li>
</ul>
<div v-html="require(`!!html-loader!markdown-loader!../assets/docs/${shownContent}.md`)"></div>
</div>
</template>
<script>
export default {
name: 'Home',
data() {
return {
shownContent: 'doc1',
}
},
computed: {
docs() {
return [
'doc1',
'doc2',
'doc3',
]
},
},
}
</script>
This way it's important to note, that the name in the array has to be the same as the markdownfile.
I followed the example as mentioned above. I put the code in a component, not App.vue
https://codesandbox.io/s/xpmy7pzyqz?file=/src/App.vue
I get the following error
[Vue warn]: Invalid prop: type check failed for prop "markdown". Expected String with value "[object Object]", got Object

How to get environment variable from Quasar Framework

The environment variables are defined in config/ directory prod.env.js and dev.env.js, how to get those variable on .vue file?
I've tried using process.env.MY_VAR assuming it's nodejs built-in library, it gives an error:
[======= ] 34% (building modules){ SyntaxError: Unexpected token (1:5)
The minimal code in .vue file:
<template>
<q-layout>
<div class="layout-view">
<button class="primary" #click="foo">
<i class="on-left">lock</i> Login
</button>
</div>
</q-layout>
</template>
<script>
export default {
data() {
return { url: '' }
}
methods: {
foo: function() {
this.url = process.env.MY_VAR // no error if commented
}
}
}
</script>
What's the correct way to get those environment variable?
In dev.env.js and prod.env.js you write something like:
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
MY_VAR: '"something"'
})
Then you can use process.env.MY_VAR in your Quasar app code.
Notice the quote + double quote. The build process in Quasar uses Webpack's DefinePlugin (https://webpack.js.org/plugins/define-plugin/) which requires a JSON encoded value. Use JSON.stringify() for more complex values like JS Objects.