Don't update component until image loaded - vue.js

I have an CurrentItem component, which shows an image and a description of some item. The item regularly changes based on events from the server. The problem I'm having is that when it does, the new item's image takes a few moments to load during which time the component just has a big blank space where the image is supposed to go, which does not look good.
What I would prefer instead is to keep showing the old item until the new item's image is loaded, and only at that point update the component. Is there a simple way to achieve that?
Some example code:
For the purposes of the question, you can think of an item as a simple JSON structure:
{
"id": "ABC123",
"description": "A fine yellow specimen",
"image": "//example.com/images/ABC123.jpeg"
}
The events from the server JSON over a WebSocket, and look something like this:
{ "nextItemId": "ABC124" }
When we receive one of those, we send a GET request to fetch the next item at //example.com/items/ABC124.json.

This is my own best shot at a solution.
Basically my idea is to have another copy of <CurrentItem /> in the DOM which has display:none; and which holds the item that is about to be the current. Then, when the image in that component is loaded, we update our actual CurrentItem. Something like this:
<template>
<CurrentItem
:key="currentItemId"
:item="currentItem"
/>
<CurrentItem
:key="aboutToBecomeCurrentItemId"
:item="aboutToBecomeCurrentItem"
#imageLoaded="onImageReady"
style="display:none;"
/>
</template>
<script>
// ...
export default {
// ...
data() {
return {
currentItem = null,
aboutToBecomeCurrentItem = null
},
methods: {
onItemUpdated(newItem) {
this.aboutToBecomeCurrentItem = newItem;
},
onImageReady() {
this.currentItem = this.aboutToBecomeCurrentItem;
}
}
}
// ...
}
</script>
I think this would work, but I would love to know it if there are simpler or more idiomatic ways to achieve the same.

Preload the image and wait for its onload event to fire before setting the item:
methods: {
onReceived(newItem) {
const image = new Image(); // Create new empty image
image.onload = () => { // CALLBACK: Will fire when image is done loading
this.item = newItem; // Will set the current item
this.isLoading = false; // Will stop animation
}
this.isLoading = true; // Start animation
image.src = newItem.image; // Begin loading
}
}
Once it's finished, the current item will be set. Here's a demo with a large image:
Vue.component('item', {
props: ['item'],
template: `
<div>
<div>ITEM (id: {{ item.id }})</div>
<div>{{ item.description }}</div>
<div><img :src="item.image" ref="img" width="100" height="100" /></div>
</div>
`
})
new Vue({
el: "#app",
data() {
return {
isLoading: false,
item: {
"id": "ABC123",
"description": "A fine yellow specimen",
"image": "https://cdn.iconscout.com/icon/free/png-256/google-470-675827.png"
}
}
},
methods: {
simulateEvent() {
this.onReceived({
"id": "ZYX987",
"description": "A funky jaundiced specimen",
"image": "https://upload.wikimedia.org/wikipedia/commons/f/fa/Very_Large_Array_in_New_Mexico.jpg"
});
},
onReceived(newItem) {
this.isLoading = true;
const image = new Image();
image.onload = () => {
this.item = newItem;
this.isLoading = false;
}
image.src = newItem.image;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<item :item="item"></item>
<!-- Simulating events -->
<button #click="simulateEvent">
Simulate event
</button>
<span v-if="isLoading">[ Loading... ]</span>
</div>

you can set a time out function until the image loads
.. codes to load new image
setTimeout(function() {
... your old image codes
... 5000 means 5Seconds
},5000);
.. codes to display new image

Related

How to change the path of the photo I clicked on, event.target?

I am new at Vue, so don't judge)
<Template>
<div>
<img :src="imgPath" #click="changePath(imgPath)">
<img :src="imgPath" #click="changePath(imgPath)">
<img :src="imgPath" #click="changePath(imgPath)">
</div>
</Template>
<script>
export default {
data: () => ({
imgPath: require('../assets/img/photo_1.png'),
imgPath1: require('../assets/img/photo_1.png'),
imgPath2: require('../assets/img/photo_2.png'),
}),
methods: {
changeAnswerImgN(imgPath) {
if (imgPath == this.imgPath1) {
this.imgPath = this.imgPath2;
} else if (imgPath == this.imgPath2) {
this.imgPath = this.imgPath1;
}
},
},
mounetd() {
this.imgPath = this.imgPath1;
},
}
</script>
The problem is that when I click on any of the photos they change the path together, but I only want to change the path of the photo I clicked on.
I tried to use event.target & $emit, but nothing had happened(
Sounds like a perfect use with an array, since now you are toggling just one value imgPath, which of course when you click on one image, all changes. So consider an array, which you iterate in template, and on click, change just that image source value:
data: () => ({
imgPath1: require('../assets/img/photo_1.png'),
imgPath2: require('../assets/img/photo_2.png'),
// iterate this in template
imgs: []
}),
methods: {
changePath(img, index) {
// toggle between the two images
let newPath = img === this.path1 ? this.path2 : this.path1;
// needed for change detection
this.$set(this.imgs, index, newPath);
}
},
mounted() {
// you want all images to initially be the first
this.imgs = Array(3).fill(this.imgPath1);
}
and template:
<template v-for="(img, i) in imgs">
<img :src="img" :key="i" #click="changePath(img, i)">
</template>
SANDBOX

Vue.js Send an index with #input event

Vue version : 3.1.1
Hey guys,
I'm working with dynamic Creation Component, which means a user can add whatever of component he wants.I create it base on this documentation dynamic component creation.
And I use this component vue image uploader.
I need to send an index when the user wants to upload the image, like this :
<div v-for="(line, index) in lines" v-bind:key="index">
{{index}}//if i log the index its 0,1,2,3 and its ok
...
<image-uploader
:preview="true"
:class-name="['fileinput', { 'fileinput--loaded': line.hasImage }]"
:capture="false"
:debug="0"
:auto-rotate="true"
output-format="blob"
accept="image/*"
#input="setImage(output , index)"
:ref="'fileUpload'+index"
>
...
And the setImage funciton :
setImage: function(output,index) {
console.log(index);
console.log(output);
return ;
this.lines[index].hasImage = true;
this.lines[index].image = output;
let formData = new FormData();
formData.append("file", output);
Ax.post(upload_route, formData, {
headers: { "Content-Type": "multipart/form-data" }
})
.then(response => {
// upload successful
})
.catch(error => console.log(error));
}
And the log result is:
The index always is 0 :(
How can i send an index when i want to upload it?
I read this passing event and index and test it but it's not working on component.
Because This is a custom event not a DOM event.
what should I do?
thanks.
Because you're actually passing the return value of setImage to the #input, not the method.
You can't just add extra parameters to setImage, as ImageUploader component just emit an image to the setImage. If you need to add extra parameters to that method, you need to create custom element that wrap ImageUploader.
It's something like this:
ImageUpload.vue
<template>
<image-uploader
:debug="0"
:autoRotate="true"
outputFormat="blob"
:preview="true"
:className="['fileinput', { 'fileinput--loaded' : hasImage }]"
:capture="false"
accept="image/*"
doNotResize="['gif', 'svg']"
#input="setImage"
v-on="listeners" />
</template>
<script>
export default {
props: {
index: {
required: true,
type: Number
}
},
data() {
return {
hasImage: false,
image: null
};
},
computed: {
listeners() {
const listeners = { ...this.$listeners };
const customs = ["input"];
customs.forEach(name => {
if (listeners.hasOwnProperty(name)) {
delete listeners[name];
}
});
return listeners;
}
},
methods: {
setImage(image) {
this.hasImage = true;
this.image = image;
this.$emit("input", this.index, image); // here, we emit two params, as index for the first argument, and the image at the second argument
}
}
};
</script>
Then, you can use that component something like this:
<template>
<div class="container">
<div v-for="(line, index) in lines" :key="index">
<image-upload :index="index" #input="setImage"/>
</div>
</div>
</template>
<script>
import ImageUpload from "./ImageUpload";
export default {
components: {
ImageUpload
},
data() {
return {
lines: ["1", "2", "3", "4"]
};
},
methods: {
setImage(index, image) {
console.log("Result", index, image);
}
}
};
</script>
See the working example: https://codesandbox.io/s/vue-template-ccn0e
Just use $event like this...
#input="setImage($event, index)"
...and you're done!

Using the same component template and updating the API data

I'm building a page that has a embeds a specific twitch stream video. I'm only displaying one video at the top of my page.
Twitch has an embed code that allows you to grab the channel you want to watch and it will display the embedded video and chat. It requires a div id to target the DOM to add the embedded video.
https://dev.twitch.tv/docs/embed/everything/
My problem is when I click on another page, that uses the same template, it doesn't replace the video. Rather, it adds another IFRAME embed video to the id. So every time I click on the page, it just adds another video to the div id.
I'm using the watch function to update other elements of the page. So when I click on another page, using the same template, the data updates correctly. Everything works and updates except for that embed video.
Is there a way to clear out that div id every time I click another another page? I apologize in advance. I've only been learning Vuejs for a couple of weeks now, and it's all rather new to me.
Here is why my template looks like:
<template>
<div class="video heading-title container">
<div class="streamWrapper">
<div class="row">
<div v-for="live in streams" class="col-12 stream-card">
<div class="twitch-vid-wrapper">
<div id="twitch-embed"></div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import appService from '../service.js'
import '../embedTwitch.min.js' // twitch video embed script
export default {
name: 'Video',
data: function() {
return {
id: this.$route.params.id,
streams: []
}
},
created() {
this.getFirstLiveStream()
this.getLiveStreams()
},
watch: {
'$route' (to, from) {
this.id = to.params.id
this.getLiveStreams()
this.getFirstLiveStream()
}
},
methods: {
getLiveStreams(game){
game = this.$route.params.id;
appService.getLiveStreams(game).then(data => {
this.live = data
});
},
getFirstLiveStream(game) {
game = this.$route.params.id;
appService.getFirstLiveStream(game).then(data => {
this.streams = data
let channelName = this.streams[0].channel.display_name
appService.getTwitchStream(channelName)
});
}
}
}
</script>
Here is the method I have in my service:
const appService = {
getFirstLiveStream(game) {
return new Promise((resolve) => {
axios.get('/kraken/streams/?sort=views&stream_type=live&game='+game)
.then((response) => {
// send variables to calc the offset
var total = response.data._total;
var query = this.calculateSingleOffset(game, total)
resolve(query)
})
})
},
getTwitchStream(channel) {
return setTimeout(function(){
new Twitch.Embed('twitch-embed', {
width: '100%',
height: 480,
channel: channel
});
}
, 500);
}
}
Thanks!
As I understood, what you need is how to assign different id for one twitch template inside each instance of the component.
The solution:
add one data property like twitchId
simply uses Date.now() to generate unique id (this method is just one demo, you can use own methods to get one ID).
then bind <div :id="twitchId"> which will embed into twitch video.
Vue.config.productionTip = false
function embedContent(elementId) {
let contents = ['I am a test', 'nothing', 'connecting', 'bla bla ...', 'Puss in boots', 'Snow White']
document.getElementById(elementId).innerHTML = '<p>' + contents[Math.floor(Math.random()*contents.length)] + '</p>'
}
Vue.component('twitch', {
template: `<div class="video">
<h2>{{name}}</h2>
<div :id="twitchId"></div>
</div>`,
props: ['name'],
data() {
return {
twitchId: ''
}
},
created: function () {
this.twitchId = Date.now().toString(16)
},
mounted: function () {
embedContent(this.twitchId)
}
})
app = new Vue({
el: "#app",
data: {
videos: [
'channel 1',
'World Cup for Football',
'StackOverFlow'
]
}
})
.video {
border: 1px solid red;
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<div>
<twitch v-for="(item, index) in videos" :name="item" :key="index"></twitch>
</div>
</div>

Filtering a list of objects in Vue without altering the original data

I am diving into Vue for the first time and trying to make a simple filter component that takes a data object from an API and filters it.
The code below works but i cant find a way to "reset" the filter without doing another API call, making me think im approaching this wrong.
Is a Show/hide in the DOM better than altering the data object?
HTML
<button v-on:click="filterCats('Print')">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
Javascript
export default {
data() {
return {
assets: {}
}
},
methods: {
filterCats: function (cat) {
var items = this.assets
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === cat)) {
result[key] = item
}
})
this.assets = result
}
},
computed: {
filteredData: function () {
return this.assets
}
},
}
Is a Show/hide in the DOM better than altering the data object?
Not at all. Altering the data is the "Vue way".
You don't need to modify assets to filter it.
The recommended way of doing that is using a computed property: you would create a filteredData computed property that depends on the cat data property. Whenever you change the value of cat, the filteredData will be recalculated automatically (filtering this.assets using the current content of cat).
Something like below:
new Vue({
el: '#app',
data() {
return {
cat: null,
assets: {
one: {cat_names: ['Print'], title: {rendered: 'one'}},
two: {cat_names: ['Two'], title: {rendered: 'two'}},
three: {cat_names: ['Three'], title: {rendered: 'three'}}
}
}
},
computed: {
filteredData: function () {
if (this.cat == null) { return this.assets; } // no filtering
var items = this.assets;
var result = {}
Object.keys(items).forEach(key => {
const item = items[key]
if (item.cat_names.some(cat_names => cat_names === this.cat)) {
result[key] = item
}
})
return result;
}
},
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<button v-on:click="cat = 'Print'">Print</button>
<div class="list-item" v-for="asset in filteredData">
<a>{{ asset.title.rendered }}</a>
</div>
</div>

vue.js: Tracking currently selected row

I have a simple table where I would like to handle click elements:
<div class="row"
v-bind:class="{selected: isSelected}"
v-for="scanner in scanners"
v-on:click="scannerFilter">
{{scanner.id}} ...
</div>
JS:
new Vue({
el: "#checkInScannersHolder",
data: {
scanners: [],
loading: true
},
methods: {
scannerFilter: function(event) {
// isSelected for current row
this.isSelected = true;
// unselecting all other rows?
}
}
});
My problem is unselecting all other rows when some row is clicked and selected.
Also, I would be interested to know, it it is possible accessing the scanner via some variable of the callback function instead of using this as I might need to access the current context.
The problem is you have only one variable isSelected using which you want to control all the rows. a better approach will be to have variable: selectedScanner, and set it to selected scanner and use this in v-bind:class like this:
<div class="row"
v-bind:class="{selected: selectedScanner === scanner}"
v-for="scanner in scanners"
v-on:click="scannerFilter(scanner)">
{{scanner.id}} ...
</div>
JS
new Vue({
el: "#checkInScannersHolder",
data: {
scanners: [],
selectedScanner: null,
loading: true
},
methods: {
scannerFilter: function(scanner) {
this.selectedScanner = scanner;
}
}
});
I was under the impression you wanted to be able to selected multiple rows. So here's an answer for that.
this.isSelected isn't tied to just a single scanner here. It is tied to your entire Vue instance.
If you were to make each scanner it's own component your code could pretty much work.
Vue.component('scanner', {
template: '<div class="{ selected: isSelected }" #click="toggle">...</div>',
data: function () {
return {
isSelected: false,
}
},
methods: {
toggle () {
this.isSelected = !this.isSelected
},
},
})
// Your Code without the scannerFilter method...
Then, you can do:
<scanner v-for="scanner in scanners"></scanner>
If you wanted to keep it to a single VM you can keep the selected scanners in an array and toggle the class based on if that element is in the array or not you can add something like this to your Vue instance.
<div
:class="['row', { selected: selectedScanners.indexOf(scanner) !== 1 }]"
v-for="scanner in scanners"
#click="toggle(scanner)">
...
</div>
...
data: {
return {
selectedScanners: [],
...
}
},
methods: {
toggle (scanner) {
var scannerIndex = selectedScanners.indexOf(scanner);
if (scannerIndex !== -1) {
selectedScanners.splice(scannerIndex, 1)
} else {
selectedScanners.push(scanner)
}
},
},
...