Lazyload Images with Knockout & jQuery
javascript, jquery, knockout.js, lazy-loading
Solution
Update: now with a working example.
My approach would be:
- let your model (stations) decide what the image URL is - either the temporary or the real one, just as you do already
- have a binding whose job is to deal with the DOM - setting that image source and handling the `load` event
- limit the lazy sweeper to simply providing the signal "you're visible now"
The viewmodel
add a `showPlaceholder` flag which contains our state:
this.showPlaceholder = ko.observable(true);
add a computed observable that always returns the currently correct image url, depending on that state:
this.imageUrl = ko.computed(function() {
return this.showPlaceholder() ? this.imageTemp() : this.imageThumb();
}, this);
Now all we have to do is set `showPlaceholder` to false whenever an image should load. More on that in a minute.
The binding
Our bindings job is to set the `<img src>` whenever the computed `imageUrl` changes. If the src is the real image, it should remove the `lazy` class after loading.
ko.bindingHandlers.lazyImage = {
update: function(element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
var $element = $(element),
// we unwrap our imageUrl to get a subscription to it,
// so we're called when it changes.
// Use ko.utils.unwrapObservable for older versions of Knockout
imageSource = ko.unwrap(valueAccessor());
$element.attr('src', imageSource);
// we don't want to remove the lazy class after the temp image
// has loaded. Set temp-image-name.png to something that identifies
// your real placeholder image
if (imageSource.indexOf('temp-image-name.png') === -1) {
$element.one('load', function() {
$(this).removeClass('lazy');
});
}
}
};
The Lazy Sweeper
All this needs to do is give a hint to our viewmodel that it should switch from placeholder to real image now.
The `ko.dataFor(element)` and `ko.contextFor(element)` helper functions give us access to whatever is bound against a DOM element from the outside:
var lazyInterval = setInterval(function () {
$('.lazy:in-viewport').each(function () {
if (ko.dataFor(this)) {
ko.dataFor(this).showPlaceholder(false);
}
});
}, 1000);
Problem
I have an image intensive site built with knockout & includes jQuery. These are in foreach loops: ``` <!-- ko foreach: {data: categories, as: 'categories'} --> <!-- Category code --> <!-- ko foreach: {data: visibleStations, as: 'stations'} --> <!-- specific code --> <img class="thumb lazy" data-bind="attr: { src: imageTmp, 'data-src': imageThumb, alt: name, 'data-original-title': name }, css: {'now-playing-art': isPlaying}"> <!-- /ko --> <!-- /ko --> ``` So basically when I create these elements, imageTmp is a computed observable that returns a temporary url, and imageThumb gets set to a real url from the CDN. And I also have this chunk of code, call this the Lazy Sweeper: ``` var lazyInterval = setInterval(function () { $('.lazy:in-viewport').each(function () { $(this).attr('src', $(this).data('src')).bind('load', function(){ $(this).removeClass('lazy') }); }); }, 1000); ``` That code goes and looks for these images that are in the viewport (using a custom selector to only find images on the screen) and then sets the `src` to the `data-src`. The behavior we want is to avoid the overhead of loading a jillion (er, actually, a few hundred) that the user won't see. The behavior we are seeing is that on first load, it looks like after `ko.applyBindings()` is called somehow the Lazy Sweeper gets clobbered and we see the images revert to the default image. Then the sweeper re-runs and we see them display again. It's not clear to us how best to implement this in a more knockout-ish way. Thoughts? Insights? Ideas? I got an answer on twitter mentioning a different lazyloading library. That did not solve the problem - the problem is not understanding how the DOM and the ko representations need to interact to set up lazyloading. I believe what I need is a better way to think about the problem of creating a knockout model that sets `imageTmp`, and responds to lazyloading based on whether it's in the viewport, and then updates the model once `imageThumb` (the real image) is loaded.