Image Gallery With Backbone

update: dive into some of the finer details regarding this post here: image gallery aftermath.

Backbone.js. By now you have probably already heard about it, and you're assuming it can solve all of your problems.

Lets start by clearing up the dust. Backbone is a backbone. Backbone. It is a skeleton, an aggregation of best practices, a great MVC implementation, but at 4.6kb (min) it cannot possibly solve all of your problems.

Also, regarding some criticism with Knockout vs Backbone arguments, many Rails and Rubyists come to backbone - it doesn't give you Rails-like productivity magically.

"Getting" Backbone

Coming from domain driven design, and having implemented the classic MVC infrastructure myself on not- so-welcoming sets of technologies, I find Backbone to be a very simple and elegant solution for implementing some of these ideas as tools for frontend/Web development.

It all becomes simple when I keep in mind the following about Backbone:

  • It is an event bus, that also wires up your MVC, and makes it alive.
  • It has Model, View, Controller (recently, Router) base classes.
  • It syncs (not discussed here)
  • It routes (not discussed here)

The Demo

The purpose of the example is to take on implementing a simple image gallery; and to show how a Model and a View can co-exist under the Backbone infrastructure -- given that it is not something that Web or back end devs are used to (however is common on the desktop).

It is not a demo about fitting in a REST backend, so some of the things (like fetch) were mocked out.

Assuming that most of us at one stage or another used a gallery either with jQuery plugins, manually, or hacked it out in the ugly old DHTML days, it makes a good base line use case.

Image Gallery

In an image gallery we have a list of Thumbs, and a Front View. In terms of functionality, we want our thumbs to jump into the front when someone clicks on those. Lets drop to code.

Models

We're going to build the Thumb model and Thumbs collection.

    var Thumb = Backbone.Model.extend({
      defaults: {
        uri: '',
        state: ''
      },
      select: function(state){
      this.set({'state': state ? 'selected' : ''});
      }
    });

    var Thumbs = Backbone.Collection.extend({
      model: Thumb
      ,
      fetch: function(){
        return _.map(urls, function(url){ return new Thumb({uri: url})});
      }
      ,
      select: function(model){
        if( this.selectedThumb() ){

          this.selectedThumb().select(false);
        }
        this.selected = model;
        this.selected.select(true);
        this.trigger('thumbs:selected');
      }
      ,
      selectedThumb: function(){
        return this.selected;
      }
    });

And in coffeescript:

    class Thumb extends Backbone.Model

      defaults:
        uri: ''
        state: ''

      select: (state) ->
        st = ''
        st = 'selected' if state
        @set('state' : st)


    class Thumbs extends Backbone.Collection
      model: Thumb

      fetch: ->
        _.map(urls, (url)-> new Thumb(uri: url))

     select: (model) ->
        @selected.select(false) if @selected?
        @selected = model
        @selected.select(true)
        @trigger('thumbs:selected')

      selectedThumb: ()->
        @selected

I'd like to discuss the coffeescript version only. We see an implementation of a Thumb and Thumbs. The most important points here are that Thumbs will trigger an event once a certain thumb on it was selected.

A triggered event is any string, conventionally named 'namespace:event'. Another note is that in CoffeeScript, @ is Javascript's this. I could also have ported underscore's _.map to a CoffeeScript comprehension but chose not to, in order to reduce friction for those of you not knowing CoffeeScript.

Looking at classic MVC, this is where we can "wire" the thumbs view and make it re-render itself.

And as a side note, you're probably beginning to realize, that CoffeeScript looks and suits Backbone much, much better -- both in terms of LOC and readability. You would be completely spot-on. PS - they come from the same author as well.

Constructing Views

Continuing on, we're going to construct the Views; FrontView, ThumbView and AppView.

    var thumbs = new Thumbs;

    var FrontView = Backbone.View.extend({
      template: _.template('<img src="<%= uri %>" />'),

      el: $('#front'),


      initialize: function(){
        this.model.bind('thumbs:selected', this.render, this);
      },

      render: function(){
        this.el.html(this.template(this.model.selectedThumb().toJSON()));
      }
    });


    var frontview = new FrontView({model:thumbs});



    var ThumbView = Backbone.View.extend({
      tagName: 'li',
      template: _.template('<img src="<%= uri %>" class="<%= state %>" />'),
      events: {
        "click" : "selectThumb"
      },
      initialize: function(){
        this.model.bind('change', this.render, this);
      },
      render: function(){
        console.log('rendering');
        $(this.el).html(this.template(this.model.toJSON()));
        return this;
      },
      selectThumb: function(){

        thumbs.select(this.model);
      }
    });

    var AppView = Backbone.View.extend({
      el: $("#container"),
      render: function(){
        _.each(new Thumbs().fetch(), 
            function(t){ 
              $('div ul').append( new ThumbView({model: t}).render().el) 
            });
      }
    });

Yet again, the CoffeeScript port:

    class FrontView extends Backbone.View
      template: _.template('<img src="<%= uri %>" />')

      el: $('#front')

      initialize: ()->
        @model.bind('thumbs:selected', @render, @) # important to give 'this' last param.

      render: ()->
        @el.html(@template(@model.selectedThumb().toJSON()))
        @ # important to give 'this' out, on rendering we'll access 'el'

    thumbs = new Thumbs
    frontview = new FrontView(model:thumbs)

    class ThumbView extends Backbone.View
      tagName: 'li'
      template: _.template('<img src="<%= uri %>" class="<%= state %>" />')
      events:
        "click" : "selectThumb"
      initialize: ()->
        @model.bind('change', @render, @)

      render: ()->
        $(@.el).html(@template(@model.toJSON()))
        @

      selectThumb: ()->
        thumbs.select(@model)


    class AppView extends Backbone.View
      el: $("container")

      render: ()->
        _.each thumbs.fetch(), (t)->
          $('div ul').append( new ThumbView(model: t).render().el)

Again, going about the CoffeeScript version (you must admit, it's easier to reason about, and therefore it must be easier to read and maintain).
A view contains a template, an element which it may mount itself on in the DOM named el, and it may contain tagName which is a tag it might create on demand.

To keep you from wondering, the template can be based on any templating engine you might want to use. Here I'm using underscore's template and for brevity I'm inlining the content.

The view can react to user action, and to model events.
You set up user initiated actions in events, and you bind to events in initialize.
This is where binding to thumbs:selected comes useful in FrontView.

Notice that some events come built in. A model knows when one of its properties were changed -- now you can understand why you are restricted to using get and set on the model; these send built in events regarding things that happen in the model.
Any view can bind on its model-generated events -- which is really the power of wiring a view to a model in MVC.

So simply: a user initiates an action on a view (which we wired through events), the model would change, and would trigger on it one or more of its properties. The view is already bound to events from a model, and it would react by re-rendering itself.

Conclusion

Last but not least, we're going to initiate a render like so: javascript window.App = new AppView window.App.render() You might notice that the gallery starts out blank - no thumb in front view. I leave it as a trivial excercise to fix :).

Luckily, this is the same in Javascript and CoffeeScript -- sans the ';'.

Hope this helped. As further reference, discussion and corrections about the code here is everything in a Gist

Any questions/corrections, get in touch dotan[ruby-instance-var-symbol]paracode.com or tweet me up: @jondot.