Use Backbone.js to speed up interactions
Client-side JavaScript frameworks are not just for building big fancy applications. Lead software engineer Matt Kelly explains how ZURB used the lightweight JavaScript framework Backbone.js to build FlickrBomb, a tool for loading Flickr photos into placeholder images
If you're looking to quickly build a little JavaScript tool, you're probably not thinking about using a framework. Easier to hack together some jQuery code rather than install and learn a new framework, right? Wrong, Backbone.js is a super lightweight glue framework that looks just like the regular old JavaScript youre used to writing.
We do a lot of static prototypes here at ZURB, because we like to be able to click through pages without having to write any backend code. Often, we would drop in drab grey placeholder images, or sometimes we would go searching Flickr for sample images to help us visualise what might go in the final draft. That is until one magical Friday, when we decided it would be awesome to write some JavaScript to solve our problems. We wanted to be able to search and select photos on Flickr, directly from the placeholder images themselves. We would call it FlickrBomb, and this is the story of how we built it using Backbone.js.
It's highly recommended that you take a quick look at FlickrBomb before reading. It's one of those “a click is worth a thousand words” type of deals. Go ahead, we'll wait.
There are a lot of JavaScript frameworks on the block these days, SproutCore, JavaScriptMVC, Spine, Sammy, Knockout. But we liked Backbone.js for this particular project for a few different reasons:
1. It's light (100% fat-free in fact)
- in weight, with the latest packed version being about 4.6kb
- in code, being just over 1,000 lines of code, it's not terribly hard to follow a stack trace down into the internals without losing your mind
2. It looks like JavaScript
- because it is JavaScript, that's it and that's all
- it uses jQuery, which even your grandma knows these days
3. Super simple persistence
Get the Creative Bloq Newsletter
Daily design news, reviews, how-tos and more, as picked by the editors.
- out of the box it persists data to a backend (via REST), but by dropping in a single plug-in it will save to local storage instead
- because it abstracts away the persistence API, we could have it persist to a REST backend by just removing the local storage plug-in
Let's get started then
Because Backbone.js is just JavaScript, all we need to do is include it along with Underscore.js on the page. jQuery is not a hard dependency for Backbone per se, but we're going to be using it so we'll include it here. We'll also link up the local storage plug-in, since we don't want to hassle with setting up a backend. Note that were directly linking the files here for simplicity, but you should always host your own assets in production.
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js"></script><script src="http://documentcloud.github.com/backbone/backbone-min.js"></script><script src="http://documentcloud.github.com/underscore/underscore-min.js"></script><script src="https://raw.github.com/jeromegn/Backbone.localStorage/master/backbone.localStorage-min.js"></script>
All the following code in this article is specific to our application, so we can include it in an app.js file, or just inline if that's your thing. Just remember to include it after Backbone. Backbone enables to abstract portions of our application, to make them both modular for easy reuse and more readable for others. To best illustrate that abstraction, were going to explain the design of FlickrBomb from the bottom up, starting with the models, and ending with the views.
Our first model
The first task were going to tackle is pulling the photos from Flickr. Modelling a FlickrImage in backbone is simple enough, we'll create a new model called FlickrImage, and add some methods to help us get different size thumbs.
var FlickrImage = Backbone.Model.extend({ fullsize_url: function () { return this.image_url('medium'); }, thumb_url: function () { return this.image_url('square'); }, image_url: function (size) { var size_code; switch (size) { case 'square': size_code = '_s'; break; // 75x75 case 'medium': size_code = '_z'; break; // 640 on the longest side case 'large': size_code = '_b'; break; // 1024 on the longest side default: size_code = ''; } return "http://farm" + this.get('farm') + ".static.flickr.com/" + this.get('server') + "/" +this.get('id') + "_" + this.get('secret') + size_code + ".jpg"; }})
Models in Backbone are objects that can be persisted, and have some functions associated with them, much like models in other MVC frameworks. The magical part of Backbone models is that we can bind events to attributes, so that when that attribute changes we can update our views to reflect that. But we're getting a little ahead of ourselves.
When we pull the photos from Flickr, we are going to get enough information to create URLs for all the sizes. However that assembly is left up to us, so we implemented the .image_url() function that takes a size parameter and returns a public link. Because this is a backbone model, we can use this.get() to access attributes on the model. So with this model, we can do the following elsewhere in the code to get the URL of a Flickr image.
flickrImage.image_url('large')
Pretty concise, eh? Since this model is specific to our application, we'll add some wrapper functions for the fullsize and thumb image sizes.
A collection of images
FlickrBomb deals with collections of images, not single images, and Backbone has a convenient way for modelling this. The aptly named Collection is what we’ll use to group Flickr images together for a single placeholder.
var FlickrImages = Backbone.Collection.extend({ model: FlickrImage, key: flickrbombAPIkey, page: 1, fetch: function (keywords, success) { var self = this; success = success || $.noop; this.keywords = keywords || this.keywords; $.ajax({ url : 'http://api.flickr.com/services/rest/', data : { api_key : self.key, format : 'json', method : 'flickr.photos.search', tags : this.keywords, per_page : 9, page : this.page, license : flickrbombLicenseTypes }, dataType : 'jsonp', jsonp : 'jsoncallback', success : function (response) { self.add(response.photos.photo); success(); } }); }, nextPage: function (callback) { this.page += 1; this.remove(this.models); this.fetch(null, callback); }, prevPage: function(callback) { if (this.page > 1) {this.page -= 1;} this.remove(this.models); this.fetch(null, callback); }});
There are a couple things to note here. First off, the model attribute tells the collections what type of model it’s collecting. We also have some attributes that we initialised for use later: key is our Flickr API key, you’ll want to replace flickrbombAPIkey with the string of your own Flickr API key. Getting a Flickr API key is free and easy, just follow this link: www.flickr.com/services/api/misc.api_keys.html. The page attribute is the current page of Flickr photos we are on.
The big method here is .fetch(), which abstracts away the details of pulling photos from the Flickr API. To avoid issues with cross-domain requests, we're using JSONP, which both the Flickr API and jQuery support. The other parameters we’re passing to the API should be self-explanatory. Of special interest are the Backbone functions being called here. In the success callback we’re using .add(), a function that takes an array of model attributes, creates model instances from those attributes, and then adds them to the collection.
The .nextPage() and .prevPage() functions first change the page we want to display,
use the collection function .remove(), to remove all the existing models from the
collection, and then call fetch to get the photos for the current page (that we just
changed).
The FlickrBombImage
Working our way back up, we need one more model to represent the placeholder image, which will consist of a collection of FlickrImages, and the current FlickrImage that has been selected. We'll call this model the FlickrBombImage.
var localStorage = (supports_local_storage()) ? new Store("flickrBombImages") : null;var FlickrBombImage = Backbone.Model.extend({ localStorage: localStorage, initialize: function () { _.bindAll(this, 'loadFirstImage'); this.flickrImages = new FlickrImages(); this.flickrImages.fetch(this.get('keywords'), this.loadFirstImage); this.set({id: this.get("id") || this.get('keywords')}); this.bind('change:src', this.changeSrc); }, changeSrc: function () { this.save(); }, loadFirstImage: function () { if (this.get('src') === undefined) { this.set({src: this.flickrImages.first().image_url()}); } }});
Since this model is responsible for keeping track of the currently selected image between page loads, it needs to know what localstorage store to use. The first line will ensure there is support for localstorage, and then create the store we will use to persist the selected image.
Backbone allows us to define an .initialize() function that will be called when an instance of the model is created. We use this function in FlickrBombImage to create a new instance of the FlickrImages collection, pass along the keywords that will be used for this image, and then fetch the images from Flickr.
The .loadFirstImage() function has been passed as a callback to run when the images have been loaded from Flickr. As you can probably guess, this function sets the current image to be the first one in the collection from Flickr. It does not do this if the current image has already been set.
We're also going to use Backbone's attribute callbacks to fire our .changeSrc() function when the src attribute of this model changes. All this callback does is call .save(), a Backbone model function that persists the attributes of the model to whatever store layer has been implemented (in our case localstore). This way, whenever the selected image is changed, it is immediately persisted.
The View Layer
Now that we've got all the backend (well, frontend backend) code written, we can put together the Views. Views in Backbone are a little different then views in other traditional MVC frameworks. While a view typically only concerns itself with presentation, a Backbone View is responsible for behaviour as well. That means that your View not only defines how something looks, but also what it should do when interacted with.
A View is commonly (but not always) tied to some data, and goes through three phases to generate presentation markup from that data:
1. The View object is initialised, and an empty element is created.
2. The render function is called, generating the markup for the view by inserting it into the element created in the previous step.
3. The element is attached to the DOM.
This may seem like a lot of work to generate some markup, and we're not even to the behaviour portion of the View yet, but it's important, and here's why. Every time you modify elements that are in the DOM, you trigger something called a browser reflow. A reflow is the browser recalculating how every thing on the page is positioned. Browser reflows can be bad for performance if called within a drag or resize event, that fires at a very short interval, but worse, they look sloppy. With complex page manipulation, you can actually see elements being added to the page, and effected elements repositioning. Following Backbone's pattern of initialise, render, and attach, you guarantee a single reflow, and the changes to the page will be perceptively instantaneous, regardless of the complexity of element manipulation.
The FlickrBombImageView
var FlickrBombImageView = Backbone.View.extend({ tagName: "div", className: "flickrbombContainer", lock: false, template: _.template('<div id="<%= this.image.id.replace(" ","") %>" ... </div>'), initialize: function (options) { _.bindAll(this, 'addImage', 'updateSrc', 'setDimentions', 'updateDimentions'); var keywords = options.img.attr('src').replace('flickr://', ''); this.$el = $(this.el); this.image = new FlickrBombImage({keywords: keywords, id: options.img.attr('id')}); this.image.flickrImages.bind('add', this.addImage); this.image.bind('change:src', this.updateSrc); }, events: { "click .setupIcon": "clickSetup", "click .flickrbombFlyout a.photo": "selectImage", "click .flickrbombFlyout a.next": "nextFlickrPhotos", "click .flickrbombFlyout a.prev": "prevFlickrPhotos" }, render: function() { $(this.el).html(this.template()); this.image.fetch(); this.resize(); return this; },...});
The functions of this view have been omitted for brevity, the source code in its entirety is available on GitHub: github.com/zurb/flickrbomb
At the top of the View, we have a couple Backbone specific attributes. tagName and className are used to define the tag and class that will be applied to this View's element. Remember that step one of View creation is creating an object, and since that creation is handled by Backbone, we need to specify the element and class. Note that Backbone has sensible defaults; if we omit these attributes, a div is used by default, and no class will be applied unless you specify one.
The template attribute is a convention, but not required. We are using it here to specify the JavaScript template function we will use to generate our markup for this view. We use the _.template() function included in Underscore.js, but you can use which ever templating engine you prefer, we wont judge you.
In our .initialize() function we are pulling out the keywords string from the image tag, and then creating a FlickrBombImage model using those keywords. We are also binding the .addImage() function to be run when a FlickrImage is added to the FlickrImages collection. This function will append the newly added FlickrImage to our image selector flyout. The last and most important line is binding the .updateSrc() function to fire when the currently selected FlickrImage is changed. When the current image is changed in the model, this function will run, update the src attribute of the image element, and CSS resize and crop the image to fit within the image dimensions specified by the user.
events: { "click .setupIcon": "clickSetup", "click .flickrbombFlyout a.photo": "selectImage", "click .flickrbombFlyout a.next": "nextFlickrPhotos", "click .flickrbombFlyout a.prev": "prevFlickrPhotos"}
Following .initialize() we have the behaviour portion of the View. Backbone provides a convenient way to bind events using an events object. The events object uses the jQuery .delegate() method to do the actual binding to the View element, so that regardless of what manipulation you do to the element inside the view, all your bound events will still work. It works just like jQuery .live(), except that instead of binding events to the entire document, you can bind them within the scope of any element. The key of each entry in the events object consists of the event and selector, the value indicates that function that should be bound to that event. Note that .delegate() does not work with some events like submit, see the jQuery .live() documentation for a complete list of supported events.
render: function() { $(this.el).html(this.template()); this.image.fetch(); this.resize(); return this;}
Lastly, we have the .render() function that is responsible for creating our markup and doing any additional work that cannot be performed until the View markup has been added to the View element. After we render our template, we need to call .fetch() on our FlickrBombImage. .fetch() is a Backbone function that gets the latest copy of the model from the persistence layer. If we had saved this model before, .fetch() would retrieve that data now. After the image has been fetched we need to call resize to position it correctly.
The Home Stretch
With all the pieces in place, all we need to do now is find the placeholder images on the page and replace them with the rendered FlickrBombImage views.
$("img[src^='flickr://']").each(function () { var img = $(this), flickrBombImageView = new FlickrBombImageView({img: img}); img.replaceWith(flickrBombImageView.render().el);});
This little snip needs to be run at the bottom of the page, or in a document ready callback, to ensure that it can find the placeholder images it will replace. We use the convention of specifying flickr://[KEYWORD] in the src attribute of a image tag to indicate it should be populated with images from Flickr. We find image elements with a matching src attribute, create a new FlickrBombImageView, and then replace the image with ours. We grab a copy of the original image and pass it to our FlickrBombView, so that we can pull some additional configuration options that may have been specified on the element.
The end result of all that hard work is a very simple API for people using the library. They can simply define image tags using the flickr:// convention, drop the FlickrBomb code at the bottom of their page, and bam, they've got placeholder images from Flickr.
Works great with big ol web apps as well
We have a big ol web app called Notable, that was written without concern for generating content client-side. When we wanted to make sections of the app turbo charged by generating content client side, we chose Backbone. The reasons were the same: we wanted a lightweight framework to help keep the code organised, but not force us to rethink the entire application.
We launched the changes earlier this year with great success, and have been singing Backbones praise ever since.
Additional resources
There is a lot more to Backbone than what I covered in this article, the C (controller) portion of MVC (model view controller) for starters, which is actually an R (router) in the latest version. And it's all covered in the Backbone documentation, a light Saturday morning read:
documentcloud.github.com/backbone/
If more traditional tutorials are your thing, then check out the very well documented code of this todo application written in Backbone:
documentcloud.github.com/backbone/docs/todos.html
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.