Vue fallback to asset images - vue.js

I have a bunch of entities I want to display on a page in a Vue 3 application. Each entity should have an image URL. If an entity does not have such a URL, I want to display a default image that's stored in /src/assets/images/default.png
So I've created a computed property that returns the image URL, either entity.url or the URL above.
Unfortunately, this doesn't work, instead of getting the default image I'm getting no image, and the Vue dev webserver returns index.html. This is because Vue's handling of static assets doesn't work when the static asset URL is returned in runtime.
The image is shown properly if I hard-code the URL #/assets/images/default.png, but not if I return this as a string (dropping the # makes no difference).
How can I make Vue handle static assets that are referenced dynamically?

I'd use v-if/v-else to show the appropriate image based on whether entity.url exists ..
<img v-if="entity.url" :src="entity.url" />
<img v-else src="#/assets/images/default.png" />

new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
imageData:{ imgUrl:'https://upload.wikimedia.org/wikipedia/commons/thumb/f/f9/Wiktionary_small.svg/350px-Wiktionary_small.svg.png',
}
},
methods: {
getUrlData(data) {
if (data && data.imgUrl) {
return data.imgUrl
}
else {
return "#/assets/images/default.png"
}
}
}
})
<div id="app">
<p>{{ message }}</p>
<img :src="getUrlData(imageData)">
</div>
Try this should work in your case! and let me know if you are getting any errors

In Vue 3, you can put an #error event on the image, so if the URL returns a 404, you can execute arbitrary logic:
<script setup>
import { ref } from 'vue';
const isProfileImageBroken = ref(false);
const handleBrokenImage = () => {
console.log('image broke');
isProfileImageBroken.value = true;
};
</script>
<template>
<i v-if="!auth.user.image || isProfileImageBroken" class="icon-user icon-lg"></i>
<img
v-if="auth.user.image && !isProfileImageBroken"
:src="auth.user.image"
class="w-6 h-6 rounded-full object-cover"
#error="handleBrokenImage"
>
</template>

Related

How to use a variable as an image src - vue.js

I'm new to Vue and done a little bit of html and css, i want to use a variable as the image directory but the image never loads, the variable is being updated by a tauri function which works and i need the image to change as well.
this is a bit of my code
<template>
<img v-bind:src=getimg()>
-- and --
<img :src = {{data}}}>
-- and --
<img src = {{data}}>
-- and much more ... --
</template>
<script setup>
var data = ref("./assets/4168-376.png")
function getimg() {
console.log(data1.value)
return require(data1.value)
}
</setup>
Based on your code, you are using Vue3 Composition API. There are a few things that are missing from your code that probably didn't made your app work.
As others have mention, you don't use curly braces in the attributes. You use
<img :src="variable"> // just use : in front of an attribute and it will consider as v-bind
<img v-bind:src="variable"> // or you directly use v-bind, less commonly used
<img :src="'static string'"> // no point doing this, but just a reference of how it works
When you are using composition API, you will have to import the functions first such as ref.
<template>
<img :src="data">
<img v-bind:src="data">
<img :src="getimg()">
</template>
<script setup>
import { ref } from 'vue'
const data = ref("./assets/4168-376.png") // prefer const over var cus this variable will never be reassigned
function getimg() {
// why are you using data1.value tho? It should be data.value
// also i don't think 'require' is needed here
return require(data1.value) // i just copy paste from your code
}
</script>
Extra: when dealing with values that does not require a parameter, usually using computed will be better. Refer Vue computed properties
<template>
<img :src="data">
<img v-bind:src="data">
<img :src="getImg">
</template>
<script setup>
import { ref, computed } from 'vue' // import it first
const data = ref("./assets/4168-376.png")
const getImg = computed(() => {
return data.value
})
Try without curly braces:
<img :src="data">
First, the syntax is:
v-bind:src /*or :src */
And not <img src = {{data}}>.
Next, the bind data should be part of the vue instance. Otherwise on binding you get an error.
Property or method 'getimg' is not defined on the instance but
referenced during render.
Last, if you want to bind the value to a function you should run the function like this:
<img :src="get_image()">
<!-- Not <img :src="get_image"> -->
Basic Snippet example (vue 2):
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: () => ({
photo_path: "https://picsum.photos/id/237/111/111",
}),
methods: {
get_image: function() {
return "https://picsum.photos/id/237/111/111";
},
}
})
/* will not work */
function not_working(){
return "https://picsum.photos/id/237/111/111";
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<!-- Import Numeral.js (http://numeraljs.com/#use-it) -->
<script src="//cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.0/numeral.min.js"></script>
<!-- Then import vue-numerals -->
<script src="https://unpkg.com/vue-numerals/dist/vue-numerals.min.js"></script>
<div id="app">
<img :src="photo_path">
<img :src="get_image()">
<img :src="not_working">
</div>
v-bind docs:
https://vuejs.org/api/built-in-directives.html#v-bind

Vue 3 Vite - dynamic image src

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.

Passing props to Vue root instance via attributes on element the app is mounted on

I am terribly new to Vue, so forgive me if my terminology is off. I have a .NET Core MVC project with small, separate vue pages. On my current page, I return a view from the controller that just has:
#model long;
<div id="faq-category" v-bind:faqCategoryId="#Model"></div>
#section Scripts {
<script src="~/scripts/js/faqCategory.js"></script>
}
Where I send in the id of the item this page will go grab and create the edit form for. faqCategory.js is the compiled vue app. I need to pass in the long parameter to the vue app on initialization, so it can go fetch the full object. I mount it with a main.ts like:
import { createApp } from 'vue'
import FaqCategoryPage from './FaqCategoryPage.vue'
createApp(FaqCategoryPage)
.mount('#faq-category');
How can I get my faqCategoryId into my vue app to kick off the initialization and load the object? My v-bind attempt seems to not work - I have a #Prop(Number) readonly faqCategoryId: number = 0; on the vue component, but it is always 0.
My FaqCategoryPAge.vue script is simply:
<script lang="ts">
import { Options, Vue } from "vue-class-component";
import { Prop } from 'vue-property-decorator'
import Card from "#/Card.vue";
import axios from "axios";
import FaqCategory from "../shared/FaqCategory";
#Options({
components: {
Card,
},
})
export default class FaqCategoryPage extends Vue {
#Prop(Number) readonly faqCategoryId: number = 0;
mounted() {
console.log(this.faqCategoryId);
}
}
</script>
It seems passing props to root instance vie attributes placed on element the app is mounting on is not supported
You can solve it using data- attributes easily
Vue 2
const mountEl = document.querySelector("#app");
new Vue({
propsData: { ...mountEl.dataset },
props: ["message"]
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Vue 3
const mountEl = document.querySelector("#app");
Vue.createApp({
props: ["message"]
}, { ...mountEl.dataset }).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app" data-message="Hello from HTML">
{{ message }}
</div>
Biggest disadvantage of this is that everything taken from data- attributes is a string so if your component expects something else (Number, Boolean etc) you need to make conversion yourself.
One more option of course is pushing your component one level down. As long as you use v-bind (:counter), proper JS type is passed into the component:
Vue.createApp({
components: {
MyComponent: {
props: {
message: String,
counter: Number
},
template: '<div> {{ message }} (counter: {{ counter }}) </div>'
}
},
}).mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0/vue.global.js"></script>
<div id="app">
<my-component :message="'Hello from HTML'" :counter="10" />
</div>
Just an idea (not a real problem)
Not really sure but it can be a problem with Props casing
HTML attribute names are case-insensitive, so browsers will interpret any uppercase characters as lowercase. That means when you're using in-DOM templates, camelCased prop names need to use their kebab-cased (hyphen-delimited) equivalents
Try to change your MVC view into this:
<div id="faq-category" v-bind:faq-category-id="#Model"></div>
Further to Michal Levý's answer regarding Vue 3, you can also implement that pattern with a Single File Component:
app.html
<div id="app" data-message="My Message"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const mountEl = document.querySelector("#app");
Vue.createApp(MyComponent, { ...mountEl.dataset }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
Or you could even grab data from anywhere on the parent HTML page, eg:
app.html
<h1>My Message</h1>
<div id="app"/>
app.js
import { createApp } from 'vue';
import MyComponent from './my-component.vue';
const message = document.querySelector('h1').innerText;
Vue.createApp(MyComponent, { message }).mount("#app");
my-component.vue
<template>
{{ message }}
</template>
<script>
export default {
props: {
message: String
}
};
</script>
To answer TheStoryCoder's question: you would need to use a data prop. My answers above demonstrate how to pass a value from the parent DOM to the Vue app when it is mounted. If you wanted to then change the value of message after it was mounted, you would need to do something like this (I've called the data prop myMessage for clarity, but you could also just use the same prop name message):
<template>
{{ myMessage }}
<button #click="myMessage = 'foo'">Foo me</button>
</template>
<script>
export default {
props: {
message: String
},
data() {
return {
myMessage: this.message
}
}
};
</script>
So I'm not at all familiar with .NET and what model does, but Vue will treat the DOM element as a placeholder only and it does not extend to it the same functionality as the components within the app have.
so v-bind is not going to work, even without the value being reactive, the option is not there to do it.
you could try a hack to access the value and assign to a data such as...
const app = Vue.createApp({
data(){
return {
faqCategoryId: null
}
},
mounted() {
const props = ["faqCategoryId"]
const el = this.$el.parentElement;
props.forEach((key) => {
const val = el.getAttribute(key);
if(val !== null) this[key] = (val);
})
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="12">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
where you get the value from the html dom element, and assign to a data. The reason I'm suggesting data instead of props is that props are setup to be write only, so you wouldn't be able to override them, so instead I've used a variable props to define the props to look for in the dom element.
Another option
is to use inject/provide
it's easier to just use js to provide the variable, but assuming you want to use this in an mvc framework, so that it is managed through the view only. In addition, you can make it simpler by picking the exact attributes you want to pass to the application, but this provides a better "framework" for reuse.
const mount = ($el) => {
const app = Vue.createApp({
inject: {
faqCategoryId: {
default: 'optional'
},
},
})
const el = document.querySelector($el)
Object.keys(app._component.inject).forEach(key => {
if (el.getAttribute(key) !== null) {
app.provide(key, el.getAttribute(key))
}
})
app.mount('#app')
}
mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app" faqCategoryId="66">
<h1>Faq Category Id: {{faqCategoryId}}</h1>
</div>
As i tried in the following example
https://codepen.io/boussadjra/pen/vYGvXvq
you could do :
mounted() {
console.log(this.$el.parentElement.getAttribute("faqCategoryId"));
}
All other answers might be valid, but for Vue 3 the simple way is here:
import {createApp} from 'vue'
import rootComponent from './app.vue'
let rootProps = {};
createApp(rootComponent, rootProps)
.mount('#somewhere')

How to use vuetify's v-img inside v-html

I would like to use <v-img> of vuetify inside v-html to replace the standard <img>, i.e.
<div v-html="'<v-img src="some_image_url" />'">
I understand that v-html is meant for only standard HTML component, and I would like to know if there is anyway to use a custom component (such as <v-img>) inside v-html.
The example below uses some cheap and cheerful RegExps to do the parsing, nothing I would use in production code. My focus was on how to avoid using v-html rather than finding a reliable way to parse out the <img> tags.
The key thing I'm trying to demonstrate is how you can parse the text into chunks and then iterate over the chunks in the template to create v-img components. I've used a dummy component for v-img but the principle would be exactly the same for the real thing.
new Vue({
el: '#app',
components: {
vImg: {
template: '<strong>[<slot/>]</strong>'
}
},
data () {
return {
text: 'Something something <img src="somepath"> and <img src="otherpath">'
}
},
computed: {
chunks () {
const re = /(<img\s[^>]*>)/g
const text = this.text
const parts = text.split(re).filter(part => part)
return parts.map(part => {
if (part.match(re)) {
const matchSrc = part.match(/\ssrc="([^"]*)"/)
return {
src: matchSrc && matchSrc[1]
}
}
return part
})
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<template v-for="chunk in chunks">
<template v-if="typeof chunk === 'string'">{{ chunk }}</template>
<v-img v-else>{{ chunk.src }}</v-img>
</template>
</div>
I managed to solve it with v-runtime-template, which can be found here:
https://github.com/alexjoverm/v-runtime-template
Cheers!

Vue.js Dynamic Component - Template not showing components data

I'm trying to build a quiz-game with VueJs and up until now everything worked out smoothly, but now that I'm started using dynamic components I'm running into issues with displaying the data.
I have a start component (Start View) that I want to be replaced by the actual Quiz component ("In Progress") when the user clicks on the start button. This works smoothly. But then, in the second components template, the data referenced with {{ self.foo }} does not show up anymore, without any error message.
The way I implemented is the following:
startComponent:
startComponent = {
template: '#start-component',
data: function () {
return {
QuizStore: QuizStore.data
}
},
methods: {
startQuiz: function () {
this.QuizStore.currentComponent = 'quiz-component';
}
}
}
};
And the template:
<script type="x-template" id="start-component">
<div>
<button v-on:click="startQuiz()">
<span>Start Quiz</span>
</button>
</div>
</script>
Note: I'm using x-templates since it somehow makes the most sense with the rest of the application being Python/Flask. But everything is wrapped in {% raw %} so the brackets are not the issue.
Quiz Component:
quizComponent = {
template: '#quiz-component',
data: function () {
return {
QuizStore: QuizStore.data,
question: 'foo',
}
};
And the template:
<script type="x-template" id="quiz-component">
<div>
<p>{{ self.question }}</p>
</div>
</script>
And as you might have seen I'm using a QuizStore that stores all the states.
The store:
const QuizStore = {
data: {
currentComponent: 'start-component',
}
};
In the main .html I'm implementing the dynamic component as follows:
<div id="app">
<component :is="QuizStore.currentComponent"></component>
</div>
So what works:
The Start screen with the button shows up.
When I click on the Start Button, the quizComponent shows up as expected.
What does not work:
The {{ self.question }} data in the QuizComponent template does not show up. And it does not throw an error message.
it also does not work with {{ question }}.
What I don't understand:
If I first render the quizComponent with setting QuizStore.currentComponent = 'startComponent', the data shows up neatly.
If I switch back to <quiz-component></quiz-component> (rather than the dynamic components), it works as well.
So it seems to be the issue that this. does not refer to currently active dynamic component - so I guess here is the mistake? But then again I don't understand why there is no error message...
I can't figure out what the issue is here - anyone?
You may have some issues with your parent component not knowing about its child components, and your construct for QuizStore has a data layer that you don't account for when you set currentComponent.
const startComponent = {
template: '#start-component',
data: function() {
return {
QuizStore: QuizStore.data
}
},
methods: {
startQuiz: function() {
this.QuizStore.currentComponent = 'quiz-component';
}
}
};
const QuizStore = {
data: {
currentComponent: 'start-component',
}
};
new Vue({
el: '#app',
data: {
QuizStore
},
components: {
quizComponent: {
template: '#quiz-component',
data: function() {
return {
QuizStore: QuizStore.data,
question: 'foo'
}
}
},
startComponent
}
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<script type="x-template" id="start-component">
<div>
<button v-on:click="startQuiz()">
<span>Start Quiz</span>
</button>
</div>
</script>
<script type="x-template" id="quiz-component">
<div>
<p>{{ question }}</p>
</div>
</script>
<div id="app">
<component :is="QuizStore.data.currentComponent"></component>
</div>
The following worked in the end:
I just wrapped <component :is="QuizStore.currentComponent"></component> in a parent component ("index-component") instead of putting it directly in the main html file:
<div id="app">
<index-component></index-component>
</div>
And within the index-component:
<script type="x-template" id="index-component">
<div>
<component :is="QuizStore.currentComponent"></component>
</div>
</script>
Maybe this would have been the right way all along, or maybe not, but it works now :) Thanks a lot Roy for your help!