I have created my own little image slider, and to get the loader working I had to create an addEventListener
and then append the loaded image into the DOM.
However, there's a bug in this scenario: When an image takes a while to load and the user clicks past it before it is loaded to see the next image, the event listener is still working in the background, and when the image is then fully loaded it overwrites what the user is currently looking at.
My HTML:
<template name="ImageGallery">
{{#each galleryImages}}
{{#if viewingImage}}
{{> Image}}
{{/if}}
{{/each}}
</template>
<template name="Image">
<div class="image-in-gallery">LOADING</div>
</template>
Checking if the "image" the user wants to see is the one we have hit in the each
iteration (thus displaying it):
Template.ImageGallery.helpers({
viewingImage: function() {
var self = this
var galleryImages = Template.parentData().galleryImages
var renderImage = false
var viewing = Template.instance().viewingImage.get()
var posOfThisImage = lodash.indexOf(galleryImages, self)
if (viewing === posOfThisImage) {
renderImage = true
}
return renderImage
}
})
Enabling the user to see the next "image" and looping if we have hit the end:
Template.ImageGallery.events({
'click .click-to-see-next-image': function(event, template) {
var viewing = template.viewingImage.get()
var imageCount = this.galleryImages.length
var nextImage = ++viewing
if (nextImage < imageCount) {
template.viewingImage.set(nextImage)
}
else {
template.viewingImage.set(0)
}
}
})
The loader:
Template.Image.onRendered({
var imageUrl = Template.currentData().name + Template.parentData().name + '.jpg'
var imageObj = new Image()
imageObj.src = imageUrl
imageObj.addEventListener('load', function() {
$('.image-in-gallery').empty()
$('.image-in-gallery').append($(imageObj))
}
})
You can see where the problem lies: the empty()
and append()
of course overwrites the image the user currently looking at and sets it to be whatever is next loaded.
I want to add a break somehow in the addEventListener
function to see if the image that is loaded is actually the one the user wants to see. But there are two problems:
1) The ReactiveVar
variable isn't available in this template. Do I need to use a Session
variable after all?
2) I have no idea how to break.
Can anyone help?
Reading through your question made me think, that maybe it is possible to rebuild your templating setup without the need of this complicated code within templates and fix the root cause - inability to understand which image is currently visible and to easily set next one to be visible.
I have found that best bet is to forget about trying to access parent data context from child templates, because ReactiveVar
is not accessible via data contexts anyway (As you have no doubt found out). And data context hierarchies are dependent on Blaze html, so it is not advised to use them in this way.
And other extreme is having Session variable, that would be very global.
Luckily there is a middle way.
Define ReactiveVar
outside of Template namespace, and then it will be equally accessible by both parent and children templates. This is works especially well, if you are trying to write a package, and do not want to pollute global Session namespace.
For example put this in hello.html:
<head>
<title>rangetest</title>
</head>
<body>
<h1>Welcome to RangeTest!</h1>
{{> hello}}
{{> rangeList}}
</body>
<template name="hello">
<p>And another test {{anotherTest}}</p>
</template>
<template name="rangeList">
{{#each ranges}}
{{> range}}
{{/each}}
</template>
<template name="range">
<p>{{name}}</p>
<input type="range" value="{{value}}" min="1" max="10">
</template>
and this in hello.js
if (Meteor.isClient) {
anotherDict = new ReactiveDict('another');
anotherDict.set('hey',2);
Template.hello.helpers({
anotherTest: function() {
return anotherDict.get('hey');
}
});
Template.rangeList.helpers({
ranges: function() {
var ranges = [
{
'name':'first',
'value': 5
},
{
'name':'second',
'value': 6
},
{
'name':'third',
'value': 7
},
{
'name':'fourth',
'value': 8
}
];
return ranges;
},
});
Template.range.events({
'input input[type="range"]': function(e,t) {
var value = parseInt(e.target.value);
anotherDict.set('hey',value);
}
});
}
you will see that reactive variables propagate nicely between templates.
Probably this does not answer your question about event listeners, but hopefully you will be able to replace your manually added event listener with the Template.Image.events({
event listeners after implementing reactivity the way I have proposed, and those will be tightly bound to particular child template and there will be no way that they would fire unpredictably.
p.s. I had example using ReactiveDict and range inputs, as this was the usecase where I needed to solve similar issue. I had all three options investigated, and this was the one that finally worked and felt more or less meteor way as well.
I think there are two flaws in your code :
You never remove your load
event when your template is destroyed. So even if your template is destroyed because you clicked to get your next image, the image is still loading in your browser and the load
event is launched. destroyed elements removeEventListener
You are using $
in your onRendered
. $
refers to the the global DOM whereas this.$ would only look for the local DOM of your template. If you had used this.$
it should have thrown an error because your local DOM was destroyed and the wrong image wouldn't have been showed.
This is how I would do it : https://github.com/darkship/ImageGallery
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With