5

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?

Yeats
  • 2,112
  • 3
  • 30
  • 46
  • 1
    Can you try to setup an jsfiddle? The code looks to scattered to make a good judgement where the so called bug is. – Ferry Kobus Dec 20 '15 at 10:48
  • When I add the full code it's "too scattered", when I don't people demand the full code. Love this site. – Yeats Dec 20 '15 at 15:45
  • There's a difference in adding a full code on the site and setting up a jsfiddle. Now I have to clone your code and create a working example myself to start helping you. When adding a jsfiddle (just a link) you'll be helped much quicker and almost always have a garanteed answer. – Ferry Kobus Dec 20 '15 at 18:02
  • @FerryKobus This is Meteor. I can't make a jsfiddle. – Yeats Dec 20 '15 at 18:08
  • so, no answer given was good enough? What was missing? – Martins Untals Dec 27 '15 at 18:39
  • @MartinsUntals Going through them now, haven't been able to check before. Thought this thread was dead when I left. – Yeats Dec 27 '15 at 18:54

4 Answers4

4

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.

Martins Untals
  • 2,128
  • 2
  • 18
  • 31
  • Don't quite know what you mean with `Template.Image.events` and what I should do there. I moved my `ReactiveVar` to the global space but I'm unsure how this isn't exactly like a `Session` now. Regardless, I'm working on something involving `removeEventListener` and your idea now. – Yeats Dec 27 '15 at 19:01
  • I need the old `imageObj` to be available so I can remove the event listener, but that seems impossible or at least very hackish. No idea how to proceed. – Yeats Dec 27 '15 at 19:16
  • Well, main point of my answer is - try to avoid doing stuff in onRendered at all. And main difference from Session is that, such reactive var would not leak outside of the package, if you are building one, and would not pollute session namespace with something that is supposed to be quite local. I am a bit suprised about it being unavailable from onCreate and onRendered though. – Martins Untals Dec 27 '15 at 19:24
  • try to go back to start and not add any event listeners yourself. and avoid doing things in onRendered. and in my answer with Template.Image.events I mean that you can use Meteor built in event listeners, without adding your own – Martins Untals Dec 27 '15 at 19:27
  • Well, I have no idea how to catch the load event without that event listener. Meteor's built in ones does not include a `load`. – Yeats Dec 27 '15 at 19:34
  • well, I suggest taking the step back and trying to solve original problem by using Meteor tools. You are fighting with consequences right now. Let meteor do all the loading and event handling. You should care only about deciding what to show to the user. And that can be solved by using reactive variables. Of course, as I do not understand what the original problem is about (what exactly forced you to start manipulating dom manuyaly in the first place), then it is hard to help more – Martins Untals Dec 27 '15 at 19:46
  • I manipulate the DOM because I want to show an image loader. I have an event listener because Meteor has no such tool. – Yeats Dec 27 '15 at 20:08
2

I see, probably the issue is the architecture itself.

You should use two different data structures. Separate the loading from the rendering.

Let's say galleryImages is the images data, you should iterate them just in order to load images on the client side and push them into a different array loadedImages.

Then the template should iterate and interacts with loadedImages.

Sorry but I'm not familiar with meteor I can not help with code.

Davide Ungari
  • 1,920
  • 1
  • 13
  • 24
2

I think there are two flaws in your code :

  1. 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

  2. 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

Community
  • 1
  • 1
user3636214
  • 605
  • 4
  • 12
  • Wait, you update the database with a `loaded` status? Are you crazy or is this some new hip thing I don't understand? – Yeats Dec 27 '15 at 18:31
  • I borrowed heavily from your code and made a pretty good image slider with loader! – Yeats Jan 04 '16 at 02:13
1

Use a dummy/fake/hidden img tag to initial load the next image, and listen to the load event of this element, after the image was loaded into this element it is cached and you can put it into viewer.

Hint: for the faked element you should use

.fake-img {visibility: hidden; position: absolute}

for the hidden image tag, some browsers are not loading images if the rule is display: none

hope it helps..

But I would also agree, that you should think about your architecture and try not to use jquery dom manipulations in meteor

webdeb
  • 12,993
  • 5
  • 28
  • 44
  • Unsure what this accomplishes. Why would I want to cache a fake image? How does this help me with the issue at hand? – Yeats Dec 27 '15 at 19:04
  • I was thinking, that you want let the previous one keep in place until the next image is loaded. A hidden img tag just lets you load the next image in the background ;) – webdeb Dec 29 '15 at 14:45