Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Capture when all images have loaded (and do something) using KnockoutJS

I currently have a basic implementation that captures when all images have loaded but I'm unhappy with the code, scalability and somewhat tight coupling between the UI and viewModel.

I have an array of promises that I've declared outside of my viewModel and bindingHandler so that each have access to it. The bindingHandler will push each unresolved promise into the array and the init function will wait until the array has received all resolved promises. Once that happens a function is called that sets a uniform height for all images.

Here's the UI:

<ul data-bind="foreach: movies">
    <li>
        <div class="image">
            <img data-bind="imageLoad: { src: posters.Detailed, alt: title }"/>
        </div>
    </li>
</ul>

The other side:

function ViewModel() {
    var self = this;
    self.movies = ko.observableArray([]);

    self.init = function(data) {
        self.isLoading(true);

        self.movies = ko.utils.arrayMap(data, function(item) {
            return new robot.ko.models.Movie(item);
        });

        $.when.apply($, promises).done(function () {
            robot.utils.setThumbnailHeight($('.thumbnails li'), function () {
                self.isLoading(false);
            });
        });
    };
}

ko.bindingHandlers.imageLoad = {
    init: function(element, valueAccessor) {
        var options = valueAccessor() || {};
        var promise = $.Deferred();

        var loadHandler = function() { return promise.resolve; };
        promises.push(promise);

        ko.applyBindingsToNode(element, {
            event: { load: loadHandler },
            attr: { src: options.src, alt: options.alt }
        });     
    }
}

In a perfect world my viewModel would just hand-off a callback to my bindingHandler but it would be dynamic, it wouldn't need to know anything about the UI and the bindingHandler would be able to figure out which element it needed to adjust. I've toyed around with the idea of some kind of deferredObservable but in the end I'm just unhappy having this in the viewModel. I've also thought about using a data- attribute on the parent element to know which element to actually set the height on, but that seems sloppy.

I feel like I'm missing something.

So, I'm asking, what is a better way to accomplish this that would allow loose coupling and scalibility so that I wouldn't have to specifically tell the UI what to do (as much) and when, from my viewModel?

like image 934
bflemi3 Avatar asked Nov 13 '22 09:11

bflemi3


1 Answers

Use a custom binding at the foreach scope that deals with it. For example:

<ul data-bind="foreach: movies, uniformHeight: movies">
    <li>
        <div class="image">
            <img data-bind="imageLoad: { src: posters.Detailed, alt: title }"/>
        </div>
    </li>
</ul>

Data-based approach

Make your assumptions based on the shape of the ViewModel - specifically, assuming the state of the promises reflects the actual state of the loaded images. The bindingHandler could look something like so:

ko.bindingHandlers.uniformHeight = {
  init: function(element, valueAccessor) {
      //assume valueAccessor contains an array of promises
      $.when.apply($, valueAccessor()()).done(function () {
            robot.utils.setThumbnailHeight($(element).find('.thumbnails li'), function () {
                self.isLoading(false);
            });
      });
  }
};

This might require a little more reorganization around how you're handling the logic in robot.ko.models.Movie but based on what you've posted, nothing drastic.

Resource-based approach

Requires more plumbing, but probably will end up being more reliable.

Your outer uniformHeight binding could register itself on the bindingContext and wait for child imageLoad bindings to each raise an event back to notify the image has been loaded; accumulate the heights as they come back and maybe debounce.

like image 150
Rex M Avatar answered Nov 14 '22 23:11

Rex M