Build an image gallery with Knockout
New JS libraries are making rapid development of complex interfaces a breeze. Stephen Fulljames builds an image gallery to showcase the power of Knockout
This article first appeared in issue 228 of .net magazine – the world's best-selling magazine for web designers and developers.
If you’re working with a fairly simple, content-based website then your JavaScript needn’t get too complicated; perhaps a lightbox effect for an image gallery and some form validation. But as soon as a reasonable amount of data or the need to keep track of UI state in your application is added to the mix then it can start to cause a bit of a problem.
For interfaces where the user can page through data, change the appearance or position of components on the page or make selections or filters that need to persist, trying to rely on DOM inspection to understand where things are is likely to end in frustration. Perhaps a better way to approach things is to have a clean separation of presentation and logic, and that’s where frameworks such as Knockout come in. You might already use event handler bindings in jQuery or other JavaScript libraries to connect user actions with parts of a page, but with Knockout we can go one step further and use JavaScript to automatically populate the interface – so whenever the data or state changes so does the UI.
In rich UI development this can greatly simplify the process of loading and updating data. It’s also more robust and testable, so whether you’re working on your own or in a team it’s a great way to make life easier for everyone.
What is Knockout?
Knockout is a JavaScript implementation of the Model-View-View Model pattern, a way to define data in a model object and then bind DOM elements or templates to that data. The three parts of the pattern are:
- The model Your data: typically this will be JSON loaded via Ajax, but there are many other ways to get data into your app, such as querying an existing DOM.
- The view Your HTML, with any element you want to populate or manipulate given a data-bind attribute. This uses the HTML5 custom data-* attribute syntax, so it passes validation but can be interpreted in HTML4 and XHTML documents as well.
- The view model The JavaScript object instance that connects everything together. These are reusable functions, so you can have multiple instances of a view model in one page, or nest a child model in a parent.
Whenever the view model changes, either through a data load or user action, the appropriate data-bound DOM elements are automatically updated so that the UI is always in sync with the view model. Bindings can also be two-way, so a value binding on a form element will update the JavaScript model object on user input, ready to be saved back to the server.
The documentation and interactive tutorials on the Knockout site are superb, so rather than repeat them I recommend you take a look and work through them to get a feel for what it can do. It’s probably worth also mentioning that the use of the data-bind attribute for templating is not to everyone’s taste, and if you’re not careful it can lead to an undesirable amount of JavaScript polluting your nice clean HTML. But there are ways to deal with this, and it’s also possible to add the attributes programmatically as you initialise your view model.
Putting it to use
A simple example of using a view model to track and update UI state is an image gallery. We have a set of images and captions to display: this is our data. There is also the need to set which is the currently selected image, along with other parameters such as whether the thumbnail area should page left and right and whether we’re at the start or end of the paging, and this is the UI state. It will be a fairly bare-bones example, but I’ll mention where it can easily be extended.
There are of course countless examples of this kind of component out there already, but most will come with their own idea of how your markup should be laid out – and to dig into the supplied CSS and start making changes can be a chore. And that’s before you discover the plug-in also does 10 things you don’t need, adding to its bulk and complexity. Surely a much better way is to start with the HTML and layout that you want and cleanly add functionality from there.
The key principle to remember when developing with a view model is that it doesn’t have, or need, knowledge of how the DOM is structured or laid out. UI updates are handled through data bindings in the HTML; if these are present the app will work regardless of how it looks. You can bind as many elements as you like to the same part of the view model, and if a binding is not made then there are no ill-effects so you can reuse the same logic in many different situations.
The view model is purely dealing with data, and this loose coupling means it’s really easy to build logical, testable components you can fit together however you like. Knockout is compatible down to IE6 and doesn’t depend on any other JavaScript library, so I’ve kept the demo framework-agnostic where possible. I’m using jQuery to initialise the page and view model, but there’s no reason why you couldn’t replace this with your framework of choice – or pure JavaScript.
Get the Creative Bloq Newsletter
Daily design news, reviews, how-tos and more, as picked by the editors.
Getting started
Let’s work through the three main parts which make up the demo. First is the data, or model, which in this case comes from a list of links to images in an HTML document.
From here we can use a DOM query to extract the URL of each image and its related caption and supply them to the view model using an initialisation function. We’ll replicate this data in a new HTML structure, so in the spirit of progressive enhancement we can hide the original markup using JavaScript during page load. In this way the basic image list will still be available to browsers that can’t apply the richer UI.
<ul class="origin"> <li><a href="img/1.jpg">Image 1 Caption</a></li> <li><a href="img/2.jpg">Image 2 Caption</a></li> <li><a href="img/3.jpg">Image 3 Caption</a></li> ... <li><a href="img/8.jpg">Image 8 Caption</a></li></ul>
If we take the terms in the MVVM pattern name in order then the view is up next, but it will make more sense to cover the view model first. This is the part that holds the data and which image is selected, and later on we’ll also deal with what happens if the user changes the selection.
var site = site || { models: { } };site.models.Gallery = function() { var self = this; this.itemsObservables = ko.observableArray(); this.init = function(data) { ko.utils.arrayForEach(data,function(item) { self.itemsObservables.push(new site.models.GalleryItem(item)); }); }}site.models.GalleryItem = function(el) { this.isSelected = ko.observable(false); this.src = el.href; this.caption = el.innerHTML;}
Habitually I create a namespace for my code; it greatly reduces the chances of conflicting with any other JavaScript in your site and so gives the freedom to call our gallery view model gallery without worrying that there might be another ‘gallery’ defined somewhere else. Knockout also creates its own namespace, ko, which is used as a container for all its own methods – similar to jQuery’s $.
The two functions following this are our view models, one for the overall gallery and one for the items inside it. As mentioned earlier, you have the flexibility to nest child models so it makes sense to break things down into separate blocks when you have functionality you want to repeat.
Inside the main view model is a Knockout observable, itemsObservables, which is where we’ll store the data for our gallery – the image URLs and captions. Creating it on this rather than as a var makes it a property of the function object, so that when we instantiate a copy of the view model later on this observable will be available as a public method – this is essential to expose it for data binding. Its also an observable array, which means that when we push or remove items to it Knockout can track this and update the UI accordingly.
By declaring ko.observableArray with an empty function call we are creating it with ‘undefined’ contents, so we should create an initialisation method in order to be able to add data to it. The next method inside the function, this.init, takes care of it.
This a function which takes a data array – in our case it’s going to be the result of a query on the DOM – and again it’s defined as a public method, within this, so that we can call it from outside the view model.
The body of the function uses a Knockout utility method, ko.arrayForEach, to loop through the data array and push each item on to itemsObservables. You could also use $.each in jQuery or _.each in Underscore – or any other method you like. Of course, once we’re inside this arrayForEach callback it has its own this scope, so in the view model itself we’ve created a variable self to be able to pass the reference in.
ko.utils.arrayForEach(data,function(item) { self.itemsObservables.push(new site.models.GalleryItem(item));});
Rather than just pushing in the item itself, which is going to be a DOM element, we’re creating an instance of the second view model, GalleryItem, which will hold the properties and observables for individual items in the gallery. This shows the advantage of splitting our view model into smaller blocks, as we can instantiate this child view model as many times as we like.
site.models.GalleryItem = function(el) { this.isSelected = ko.observable(false); this.src = el.href; this.caption = el.innerHTML;}
First we create a single Knockout observable isSelected which, as may be obvious, is whether or not this item is selected. Instead of making it ‘undefined’ with an empty function call, we’ll default it to false by passing in the value when we create the observable.
Then (and here we are relying on having an a element passed in, but you could test for others if required) we set this.src to the element’s href attribute and this.caption to its innerHTML. These are simple variables, rather than observables, because we’re not expecting them to change and therefore don’t need the overhead of keeping them in Knockout’s observable chain. And the reason we’re doing this at all is that we’re extracting the data from the element and storing it in an abstract object so we can apply it again any way we like.
At a basic level, this is all we need in our view models to create a simple gallery. Now let’s look at the HTML template for the UI, or View, where we will data-bind its observables:
<div class="gallery" data-bind="foreach: itemsObservables"> <div class="item"><img width="800" height="533" data-bind="attr: { 'src': src,'alt': caption }" /></div></div>
You can see that we’ve set up a container element, a div with the class gallery, and within this is a template, div.item. Previous versions of Knockout required you to embed these templates within script elements, which from the point of view of clean HTML wasn’t very satisfactory, but in the current 2.0 version this is no longer necessary. If you wish you can even remove container elements, by using control flow bindings in specially formatted HTML comments, but we won’t cover that here.
On the container is a data-bind attribute with a value of foreach: itemsObservables, which is telling Knockout to loop through that observable array and apply the template to whatever items are inside it. The items are the instances of the GalleryItem view model we created in the init function, so the data binding on the image element within the template can access the src and caption values inside each one and set the element attributes accordingly.
As the observable array is empty before we call the init method, at that point there will be no div.item elements in the DOM – the empty template is simply stored. If we start to add or remove items to the array, the data-binding causes copies of these template elements to be created or destroyed, all automatically.
The last step to make all this work is to create an instance of the Gallery view model on page load and populate it with our DOM element array. I’m using jQuery in a ready function for this, but feel free to substitute your library and technique of choice:
$(function() { var viewModel = new site.models.Gallery(); viewModel.init($('ul.origin a')); ko.applyBindings(viewModel);});
Here we create a variable viewModel, which is a new copy of the Gallery view model, and then call the init method, passing in the result of a DOM query for all the links within our list of items. Finally we use a Knockout method to apply the data in the view model to all the bindings in our templates. By default it will apply to the body element, but you can pass an additional argument targeting anywhere in the page DOM to restrict the scope of the bindings, for instance if you want multiple independent view models in one page. Once this is done, any changes to the view model will be instantly reflected in the UI, and vice-versa.
Moving on
At this point you have a working MVVM application, but viewing it in a browser would highlight that it isn’t very gallery-like, because all the template does is loop through the items and display their images in sequence. We still need a way for the user to be able to see which image in the list is selected and to change the selection, and, most importantly, to only show one main image at a time!
To achieve the first part of this, we’ll make use of the principle that the same data can be bound in the DOM several times, and set up a new template for a thumbnail strip:
<div class="controller"> <ul data-bind="foreach: itemsObservables"> <li data-bind="css: { 'selected': isSelected }, click: $parent.select"> <img data-bind="attr: { 'src': src}" width="140" /> <span data-bind="text: caption"></span> </li> </ul></div>
We create this using the same foreach Knockout binding to display as many list items as there are items in the observable array. We’re also outputting the same image src and captions again, but in a different markup pattern, showing the flexibility of the view model approach. (I’m using a squashed version of the main image as the thumbnail for ease, but I would expect a production site to have right-sized thumbnails.)
The first binding on the thumbnail list item is css: { 'selected': isSelected }, which is used to conditionally apply a CSS class – it will only appear on the element if isSelected is true and so indicate the currently selected item. When we created the GalleryItem view model we made this observable false by default, so for now the class will not be applied. The css binding has a slightly counter-intuitive name – it deals with classes – but if you want to bind individual CSS properties you can also use the style binding.
To make this useful, there is also a new concept on the list item; a binding to $parent.select on the click event. If you use Knockout for event handling it will take precedence over the default DOM event, and also any other event listeners that may be on this element, but you can pass control back to them later if you need to by returning true from the function we’re about to create.
The $parent prefix of the function assignment is there because, in the item template, we’re in the context of the GalleryItem view model, and by using this we can access its parent view model, the instance of Gallery, and call a function select – which we’ll define there. It could go in the GalleryItem view model and be called directly (using data-bind="click: select"), but doing so would mean creating a copy of it with each item, and there’s a further advantage to putting it a level up, too.
this.select = function(data,e) { self.setSelected(newSelection); e.preventDefault();}this.setSelected = function(newSelection) { ko.utils.arrayForEach(self.itemsObservables(),function(item) { item.isSelected(item == newSelection); });}
Actually there are two new functions here – select, which handles the click event and then calls setSelected, which actually makes the selection. It’s not essential to split things up this way, but by creating a separate setSelected method we can test it independently without needing to simulate a DOM event.
Knockout’s event bindings provide two default arguments. The first, data, is a snapshot of what the target element is bound to; in this case the relevant instance of the GalleryItem view model. The second, e, is the original DOM event. Our function calls setSelected with this and prevents the default action. As we clicked on a list item in our example there is no default action, so it’s not really necessary, but if we change the template to use a link it won’t catch us out.
We could simply set isSelected on the new selection to true, which will update the UI instantly – but any previous selection would still be active, and if we want to restrict our UI to show one main image at a time and also have an indicator in the thumbnail strip then this would be a problem.
To prevent this, we loop through all the instances of GalleryItem in the itemsObservable and compare them to the new selection. The result of this comparison is a Boolean – false or true – so we can assign it straight to isSelected by calling the observable with the comparison as the argument. In this way only one selection can be made at a time, and the item that does have isSelected set to true will now get the CSS class selected applied. This is also where the advantage of putting the selection logic in the main Gallery view model becomes clear, because from this level we can easily step in to any of its own properties – including all the items in itemsObservables.
The final use for isSelected is in order to conditionally set the visibility of the main images.
<div class="gallery" data-bind="foreach: itemsObservables"> <div class="item" data-bind="visible: isSelected">...</div></div>
We can do this by adding a visible binding to isSelected on div.item. This works by directly manipulating the style of the element, so any item where isSelected is false will have its CSS display rule set to none, and as the view model changes so will the visibility of the items.
Again, if we view in the browser things aren’t quite what we’d expect from a gallery. Normally the first image in the set would be selected by default, but as it stands we’re initialising all the items to have isSelected set to false, so no large images are visible until the user selects one. To get round this, in the main view model init method let’s set isSelected on the first item to true so it will display.
this.init = function(data) { var self = this; ... this.itemsObservables()[0].isSelected(true);}
As well using Knockout’s internal methods (such as push, which is its own rather than the pure JavaScript push) on the itemsObservables array, we can also call it with () and then access any of its items just as we would a regular array. We assign values to regular observables by calling them with the value as the argument, so the new line in the init function now sets isSelected in the first item in the observable array to true.
Making it fit
By now we have a minimal but functional image gallery. Only one image from a set is shown at once, and there’s a display of thumbnails that the user can click on in order to select which is displayed.
You will notice, though, that with the main images set to 800px wide, the thumbnail strip overflows that width – or may even wrap, depending on the size of the browser. It would be better if we could constrain the width of the strip to the image size, and let it scroll left or right depending on where the selection is. Of course, the 800px is an arbitrary figure for this demo. It could be any size, or even responsive, but dealing with this type of situation is where Knockout really comes into its own.
A bunch of new observables are needed to add this behaviour to the UI, so we’ll create a whole new view model, ScrollableArea, to store and track them – and nest this within our main view model when we define that.
site.models.Gallery = function() { var self = this; this.itemsObservables = ko.observableArray(); this.measureContent = null; this.scrollable = new site.models.ScrollableArea(); ...}
There’s another new property here to mention, measureContent. You’ll see it’s set to null and is basically a placeholder for a function we’ll define in our document ready code, so we can take advantage of some jQuery features without getting it tied up in our framework-agnostic view model. Everything else to do with our extended functionality will go in the ScrollableArea view model.
site.models.ScrollableArea = function() { var self = this; this.scrollThreshold = ko.observable(0); this.contentSize = ko.observable(0); this.scrollValue = ko.observable(0); this.scrollClickStep = ko.observable(400); this.isScrollable = ko.computed(function () { return self.contentSize() > self.scrollThreshold(); });}
The first chunk of observables here keep track of the state of the UI. scrollThreshold is the overall width of the image gallery. In practice this will be 800 but this is a generic view model so we’ll initialise it to 0; the real size can be passed in during the document ready function. contentSize is again initialised to 0, and this will be a measurement of the total width of all thumbnail items. Later on we’ll compare these values to see if the thumbnail area should scroll or not.
Next is scrollValue, this is a record of where the ‘left’ position of the thumbnail strip should be and again defaults to 0. Finally, scrollClickStep is a setting for how much the thumbnail strip should be moved by when we go left or right, and for our demo its defaulted to 400 (pixels).
After this we move on to the clever stuff, where the power of Knockout really becomes apparent. So far we’ve dealt with ko.observable and ko.observableArray, but there is a third type of observable, ko.computed, which can watch any number of other observables and will return a computed value based on them whenever any of them change. These can either be read-only computations, or two-way read/write functions.
Rather than create isScrollable with a simple value, we instead call it with a function which in this case returns a comparison of two of our previous observables, contentSize and scrollThreshold, to work out if the overall width of the thumbnail strip is larger than the space we have available to display it in. At initialisation time both of these values are 0, so it will be false – not scrollable – but as soon as we measure the DOM and put some real values in there it will automatically recalculate and anything in the template bound to this computed observable will respond. The practical way to this is to extend the document ready function we already use to set up the main view model instance:
$(function() { var viewModel = new site.models.Gallery(); viewModel.init($('ul.origin a')); ko.applyBindings(viewModel,$('body').get(0)); var c = $('div.controller'); viewModel.measureContent = function() { return c.find('li').width() * c.find('li').length; } viewModel.scrollable.contentSize(viewModel.measureContent()); viewModel.scrollable.scrollThreshold(c.width());});
Now we’ve added a variable c, which caches a DOM query for the element containing our thumbnail strip list. The function viewModel.measureContent (remember we created this as a null function on the Gallery view model earlier) is now defined as simply returning the pixel width of the first list item in the controller multiplied by the number of items, to give the total size of the strip. It’s risky to rely on all the items being the same width, but for this demo it’ll do.
This function is used to set the value of the observable scrollable. scrollThreshold. Note that we can set values directly into a nested level of the view model, because the instance of ScrollableArea was defined as a public method and its observables are also public. We also set scrollable. scrollThreshold to the width of the container itself. It’s important to set these values after the view model bindings have been applied to the DOM, to make sure we are measuring the fully rendered result rather than empty templates.
Changing the value of either of these observables causes the isScrollable observable to be recalculated, and if the content size is now wider than the space we have available it will become true.
<div class="controller"> <ul data-bind="foreach: itemsObservables, style: { 'width': scrollable.contentSize() + 'px' }"> ... </ul> <button data-bind="visible: scrollable.isScrollable, enable: scrollable.canScrollLeft, click: scrollable.scrollContent" data-direction="left">«</button> <button data-bind="visible: scrollable.isScrollable, enable: scrollable.canScrollRight, click: scrollable.scrollContent" data-direction="right">»</button></div>
To make use of isScrollable in the UI, we’ll also modify the thumbnail strip template. You’ll see that there is a style binding on the list to set its width to our previous calculation; this is to make sure the whole thing is laid out on one line even though the container element is going to be constrained to a width with overflow: hidden applied. The rest of the item template is the same as before but we’ve also added two button elements, both of which have a visible binding to isScrollable. You can probably work out what effect this has; the buttons will be set to display: none until isScrollable becomes true, at which point they will automatically pop into view.
There’s also an enable binding on these buttons. I won’t go into the detail of the computed observables which calculate if they are true or false, but they contain the logic that compares the current scroll position with the potential maximum and minimum values and prevents the user from scrolling left at the left end of the thumbnail strip, and vice versa.
Finally the buttons also have a click binding to a function scrollContent, which adds or subtracts the scroll step amount to the current scroll value depending on the direction clicked. This ultimately sets another computed observable on the ScrollableArea view model, calculatedScrollValue.
this.calculatedScrollValue = ko.computed({ read: function () { return (self.isScrollable()) ? self.scrollValue() : 0; }, write: function (value) { self.scrollValue(value); }});
Here is an example of a two-way computed observable, which contains callback functions to both read and write values. In the read function it will react to changes to isScrollable, but if we call it with a value in the argument then the write function is triggered to set scrollValue, which we can use in a style binding on the template to position the thumbnail strip.
Wrapping up
Quite a lot of concepts have been introduced quickly in this demo, and you would be forgiven if some of them have done more to confuse than illuminate. But hopefully you’ve seen some of the benefits to using a MVVM pattern and are keen to read up further on it. As we said at the start, the documentation and tutorials on the Knockout site are fantastic and well worth working through.
There’s a whole load more depth to it too, including customised bindings that can hook directly into jQuery or other libraries’ effects methods. So rather than a simple visible binding you can define your own fadeVisible to have elements appear and disappear with a bit more style than just popping in or out or view.
The key thing to remember is that throughout everything described we’ve aimed to maintain a clean separation of UI and logic. So we could totally replace the view, as long as the bindings were transferred, and it would all still work.
There are of course countless, significantly more feature rich, gallery plug-ins out there for any JavaScript framework you’d care to mention. But this is only the starting point of our Knockout-powered gallery, and for me the advantage of doing it this way is that you end up with a compact yet extensible controller in the form of the view model, which can be applied to any UI without having to worry about how nicely it will play with your existing page.
This a revised, expanded version of an article first published at 12devsofxmas.co.uk
Discover 101 CSS and JavaScript tutorials to power up your skills at Creative Bloq.
Thank you for reading 5 articles this month* Join now for unlimited access
Enjoy your first month for just £1 / $1 / €1
*Read 5 free articles per month without a subscription
Join now for unlimited access
Try first month for just £1 / $1 / €1
The Creative Bloq team is made up of a group of design fans, and has changed and evolved since Creative Bloq began back in 2012. The current website team consists of eight full-time members of staff: Editor Georgia Coggan, Deputy Editor Rosie Hilder, Ecommerce Editor Beren Neale, Senior News Editor Daniel Piper, Editor, Digital Art and 3D Ian Dean, Tech Reviews Editor Erlingur Einarsson and Ecommerce Writer Beth Nicholls and Staff Writer Natalie Fear, as well as a roster of freelancers from around the world. The 3D World and ImagineFX magazine teams also pitch in, ensuring that content from 3D World and ImagineFX is represented on Creative Bloq.