Replace image with placeholder on src change - vue.js

I have an image with a bound src value
<img :src="image.url">
When image.url gets updated, the old image stays in place until the new image is loaded. This can look a little weird as the new image lags behind the text details on change.
How can I replace the image with a loading indicator when the src property changes with vue.js?
I could add DOM manipulation with the code that triggers the change and that works, but I'd rather do it properly with vue bindings.

This should get you on the right track:
Have an imageLoading attribute in your data() function.
Set your :src as follows: <img :src="imageLoading ? ./myPlaceholder.png : image.url" />
$watch your image.url to set the loading image whenever it changes:
watch: {
'image.url': function() {
this.imageLoading = true;
}
Add an #load handler to your img tag to set imageLoading to false when the image loads: <img :src="imageLoading ? ./myPlaceholder.png : image.url" #load="imageLoading = false" />
Note on the #load handler - shamelessly stolen from this question, which does link to an example.

This is a basic image placeholder component that uses slots to display any placeholder content
<template>
<div class="image">
<img :src="src" #load="ready = true" v-show="ready">
<slot v-if="!ready" />
</div>
</template>
<script>
export default {
props: ['src'],
data: () => ({
ready: false
}),
watch: {
src () { this.ready = false }
}
}
</script>
to use it
<image-placeholder src="IMAGE_URL">
<b>PLACEHOLDER CONTENT HERE</b>
<p>Can be anything</p>
<img src="ANOTHER_IMAGE_URL" />
</image-placeholder>

Related

I know how to call images by mentioning object manually, but I want to call clicked image into new componet in vue js

I know how to call images by mentioning object manually, but I want to call clicked image into new componet in vue js.
I tried calling images manually, it worked, but I want to call image into child componet which is clicked on parent component ( Images are dynamic)
**Main Component**
<router-link to="" v-for="(image, index) in images" :key="index">
<div class="imgBox">
<img v-if="image.name.startsWith('assamese')"
:src="image.src"
:alt="image.name"
v-on:click="goTochild(image.name)"
/>
</div>
</router-link>
<script>
export default{
computed: {
images() {
const imageContext = require.context(
"#/assets/images/",
true,
/\.(png|jpg|jpeg|gif)$/
);
const images = [];
imageContext.keys().forEach((src, index) => {
images.push({
id: index,
src: imageContext(src),
name: src.split("/").pop().split(".")[0],
});
});
return images;
},
},
methods: {
goTochild(prodId) {
let imgId = prodId;
// this.$router.push({ name: "details" });
this.$router.push({ name: "child", params: { Iid: imgId } });
},
},
};
</script>
**Child Component**
<div v-for="(image, index) in images" :key="index">
<div v-if="imgId == image.id">
<h1>{{ image.src }}</h1>
<img width="250" :src="image.src" />
</div>
</div>
<script>
export default{
data() {
return {
imgId: this.$route.params.Iid,
title: "child",
};
},
};
</script>
This is not displaying image
----Main Component
enter image description here
enter image description here
---Child Component
enter image description here
This is displaying image(But I dont want to mention image details manually in object / array)
---Main Component
enter image description here
enter image description here
---Child Component
enter image description here
enter image description here
Router index file
enter image description here
I tried calling images manually, it worked, but I want to call image into child componet which is clicked on parent component ( Images are dynamic)
To call a clicked image into a new component in Vue.js, you can pass the clicked image as a prop to the child component. Here is an example of how you can achieve this:
In the parent component, when an image is clicked, emit an event with the clicked image as the payload:
<div v-for="(image, index) in images" :key="index">
<img width="250" :src="image.src" #click="handleClick(image)" />
</div>
<script>
export default{
data() {
return {
images: [{id: 1, src: 'image1.jpg'}, {id: 2, src: 'image2.jpg'}, ...],
};
},
methods: {
handleClick(image) {
this.$emit('image-clicked', image);
}
},
};
</script>
In the parent component's template, use the v-bind directive to pass the clicked image as a prop to the child component:
<child-component v-bind:clicked-image="clickedImage"></child-component>
<script>
export default{
data() {
return {
clickedImage: null,
};
},
created() {
this.$on('image-clicked', (image) => {
this.clickedImage = image;
});
},
};
</script>
In the child component, use the props property to receive the clicked image as a prop, and display the image in the template:
<template>
<div>
<img width="250" :src="clickedImage.src" />
</div>
</template>
<script>
export default{
props: ['clickedImage'],
};
</script>
This way, when an image is clicked in the parent component, the clicked image is passed as a prop to the child component, and the child component can display the image.
It's important to note that the above example is just one way to pass the clicked image to the child component, there are other ways to do it, like using a store such as vuex, or passing the prop directly through the router.

Dynamic rendering of images with v-for?

I want to render images from a local folder with v-for, but how to make it 100% dynamic?
I tried the solutions offered in this thread. When I tried the most useful solution, I just get a blank page, unless I fill the array with the name of the images.
<template>
<div class="comp__cardroster">
<div class="container__cards" >
<div v-for="image in images" :key="image" class="tile--outer">
<img class="tile--inner" :src="selectImage(image)" :alt="image"></div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
images: []
}
}
methods: {
selectImage(image) {
return require('#/assets/card-images/' + image + ".jpg")
}
}
}
</script>
The code above gives me a blank page. But when I fill the array with values, like below, I do get a result. But I don't want this obviously.
data() {
return {
images: [1,2,3,4,5,6,7,8,9,10]
}
}
I would want the code to render the images dynamically, no matter how many images I do have in my "assets/card-images" folder and without having to manually add values in the array each time I add a new image to the folder.
What am I doing wrong? I thank you for any advise.
UPDATE
Things I tried;
moving the "images" array from data to computed
moving "selectImage" method from methods to computed
moving both "images" array and "selectImage" method into computed
Either I get a blank page, or I get the same result as before
I don't think I was clear enough with my comments, so I'll explain what I meant with an example; you should be able to apply this to your use case with minimal effort. My src file structure and a screenshot of the result are also at the bottom of this answer.
The template I've made is basically the same as yours (without the extra divs with classes):
<template>
<div>
<div v-for="image in images" :key="image">
<img :src="selectImage(image)" :alt="image" />
</div>
</div>
</template>
Here's my script. I'll go through it all below:
<script>
export default {
name: 'app',
computed: {
images: function() {
const x = require.context('#/assets/card-images/', true, /\.png$/)
return this.importAll(x)
}
},
methods: {
importAll(r) {
return r.keys().map(x =>
x.substring(2, x.length) // remove "./" from file names
)
},
selectImage(image) {
return require('#/assets/card-images/' + image)
}
}
}
</script>
computed
The computed section is where you define your dynamically generated or computed values. Since you want your images to be dynamically generated, I've made images a computed function (can probably just be a value, you can play around with that).
All that images does is it uses require.context to get a list of all of the .png images in my #/assets/card-images/ folder, and trims the first couple of characters from them.
methods
importAll just retrieves and trims the image names. I've done this because otherwise, it'll think the images are at #/assets/card-images/./xxxxx.png - there's probably a better way of doing this but it works well enough.
selectImage gets an image from the file name you pass in (if it exists). If the image name doesn't exist, this will break but that shouldn't happen with how this is implemented.
Note: You can technically shorten the v-for loop by putting it directly on the img tag if you really want to, though I'd argue this is less readable:
<template>
<div>
<img v-for="image in images"
:key="image"
:src="selectImage(image)"
:alt="image" />
</div>
</template>
Here is my src folder structure. It doesn't matter what the images are called, as long as they have the same extension as you're using in your script tag:
Here is what the code prints out (all of the images are just copies of the Vue logo):
EDIT
If you want to keep your initial images array, you can move the computed stuff into the lifecycle method mounted or created (depending on your use-case). Read more about lifecycle methods here or here. Here's what my component would look like with the calculations in mounted:
<template>
<div>
<div v-for="image in images" :key="image">
<img :src="selectImage(image)" :alt="image" />
</div>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
images: []
}
},
mounted() {
const x = require.context('#/assets/card-images/', true, /\.png$/)
this.images = this.importAll(x)
},
methods: {
importAll(r) {
return r.keys().map(x =>
x.substring(2, x.length) // remove "./" from file names
)
},
selectImage(image) {
return require('#/assets/card-images/' + image)
}
}
}
</script>
Use a requireAll method to get an array, and your count. Or a custom loader.
How to load all files in a directory using webpack without require statements
node.js require all files in a folder?

autohide an element in VueJS after 1 second

OK, so I'm new to Vue ( basically, new to JS in general, but I'm playing with Vue right now ) and what I want to do, is to auto hide an element ( not on click ) inside the template tag of a component. In jQuery this would look like:
$(function() {
setTimeout(function() {
$(".hideElement").hide()
}, 1000);
});
but how this works in Vue? my component looks like this:
<template>
<div>
<h1 class="hideElement"> HELLO </h1>
</div>
</template>
<script> // nothing here
</script>
<style> // nothing here
</style>
I know how to toggle the element on click of a button, but I just want to auto hide it after 1 second without any click events everytime the users enter this component ( which is a new "page" )
You could just add a property in the data object and use v-show directive to determine whether the element should be visible or not. If the boolean is false the element is hidden, if true the element is visible.
Method Created called synchronously after the instance is created.
<template>
<div>
<h1 v-show="elementVisible" class="hideElement"> HELLO </h1>
</div>
</template>
<script>
export default {
data() {
return {
elementVisible: true
}
},
created() {
setTimeout(() => this.elementVisible = false, 1000)
}
}
</script>

Vue - Change in the state does not re render the template?

I'm trying to understand the basics of Vue and so far what I understand is every time any of the states in the data property changes, the template or the component should re render. Here is the code snippet I'm working with.
index.html
<div id="app">
<h3>Generator</h3>
<div>
Input:
<input #input="onInput"/>
</div>
<div>
Output:
{{test()}}
</div>
</div>
main.js
new Vue({
el:'#app',
data: {
textInput: ''
},
methods: {
onInput(event){
this.textInput = event.target.value
},
test(){
console.log("Test running")
}
}
})
What I expected to happen?
Since I'm updating the textInput data property with every keystroke, I thought that since the template would re render itself, I would see the Test running message in the console every time I hit a key and since the page would re render every time, I would see the input field as blank.
What currently happens
I see the test function run only once when I run the code.
I don't see a blank input field with every key stroke
The DOM does not depend on textInput, so changes to it do not cause a re-render. If the render function uses the variable, you will get a re-render when the variable changes.
new Vue({
el:'#app',
data: {
textInput: ''
},
methods: {
onInput(event){
this.textInput = event.target.value;
},
test(){
console.log(this.textInput);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h3>Generator</h3>
<div>
Input:
<input #input="onInput"/>
</div>
<div>
Output:
{{textInput.length}}
{{test()}}
</div>
</div>

Vue fallback to asset images

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>