Skip to content

azat-co/ga-backbone

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Introduction to Backbone

WARNING!

This repo is not supported/update anymore, please refer to the latest and the better version at https://leanpub.com/rapid-prototyping-with-js/read#intro-to-backbonejs.

Previous Classes

Previous classes

Content

  1. Setting up Backbone.js App From Scratch
  2. Working with Collections
  3. Event Binding
  4. View and Subviews with Underscore.js
  5. ADM and Require.js for Development
  6. Require.js for Production
  7. Super Simple Backbone Starter Kit

Setting up Backbone.js App From Scratch

We're going to build a typical starter "Hello World" application using Backbone.js and Mode-View-Controller (MVC) architecture. I know it might sound like an overkill in the beginning, but as we go along we'll add more and more complexity, things like Models, Subviews and Collections.

A full source code for "Hello World" app is available at GitHub under github.com/azat-co/ga-backbone/hello-world.

Dependancies

Download the following libraries:

And include these frameworks in the index.html file like this:

<!DOCTYPE>
<html>
<head>
  <script src="jquery.js"></script>
  <script src="underscore.js"></script>
  <script src="backbone.js"></script>

  <script>
    //TODO write some awesome JS code!
  </script>

</head>
<body>
</body>
</html>

Note: We can also put <script> tags right after the </body> tag. This will change the order of impact performance in large files.

Let's define a simple Backbone.js Router inside of <script> tags:

...
    var router = Backbone.Router.extend({
    });
...

Note: For now, to Keep It Simple Stupid (KISS) we'll be putting all JavaScript code right into our index.html file. This is not a good idea for a real development or production code. We'll refactor the code later.

Then set up a special routes property inside of the extend call:

    var router = Backbone.Router.extend({
      routes: {
      }
    });

Backcone.js Routes need to be in the following format: 'path/:param':'action' with will result in path/param` calling function action which we'll set up as a property to extend later. For now we add a single home route:

    var router = Backbone.Router.extend({
      routes: {
        '': 'home'
      }
    });

This is good, but now we need to add a home function:

    var router = Backbone.Router.extend({
      routes: {
        '': 'home'
      },
      home: function(){
        //TODO render html
      }
    });

We'll come back to the home function later to add more logic for creating and rendering of a View. Right now we should define our homeView:

    var homeView = Backbone.View.extend({
    });

Looks familiar, right? Backbone.js uses similar syntax for all of its components: extend function and a JSON object as a parameter to it.

There are a multiple ways to proceed from now on, but the best practice is to use el and template properties which are "magical", i.e., special in Backbone.js:

    var homeView = Backbone.View.extend({
      el: 'body',
      template: _.template('Hello World')   
    });

Property el is just a string that holds jQuery selector (you can use class name with '.' and id name with '#'). Template property has been assigned an Underscore.js function template with just a plain text 'Hello World'.

To render our homeView we use this.$el which is a compiled jQuery object referencing element in an el property, and jQuery .html() function to replace HTML with this.template() value. Here is how the full code for our Backbone.js View looks like:

    var homeView = Backbone.View.extend({
      el: 'body',
      template: _.template('Hello World'),
      render: function(){
        this.$el.html(this.template({}));
      }
    });

Now, if we go back to the router we can add these two lines to the home function:

    var router = Backbone.Router.extend({
      routes: {
        '': 'home'
      },
      initialize: function(){
        
      },
      home: function(){
        this.homeView = new homeView;
        this.homeView.render();
      }
    });

The first line will create homeView object and assign it to homeView property of the router. The second line will call render method in homeView object triggering 'Hello World' output.

Finally, to start a Backbone app we call new Router inside of a document ready wrap to make sure that file's DOM is fully loaded:

  var app;
    $(document).ready(function(){
      app = new router;
      Backbone.history.start();      
    })

Here is the full code of the index.html file:

<!DOCTYPE>
<html>
<head>
  <script src="jquery.js"></script>
  <script src="underscore.js"></script>
  <script src="backbone.js"></script>

  <script>

    var app;
    var router = Backbone.Router.extend({
      routes: {
        '': 'home'
      },
      initialize: function(){
        
      },
      home: function(){
        this.homeView = new homeView;
        this.homeView.render();
      }
    });

    var homeView = Backbone.View.extend({
      el: 'body',
      template: _.template('Hello World'),
      render: function(){
        this.$el.html(this.template({}));
      }
    });

    $(document).ready(function(){
      app = new router;
      Backbone.history.start();      
    })

  </script>
</head>
<body>
  <div></div>
</body>
</html>

Open index.html in the browser to see if it works, i.e., 'Hello World' message should be on the page.

Working with Collections

The full source code of this example is under ga-backbone/collections.

Follow all the steps from Setting up Backbone.js App From Scratch exercise or download the app from ga-backbone/hello-world.

We should add some data to play around with, and to hydrate our views. To do this, add right after script tag and before the other code:

   var appleData = [
      {
        name: "fuji",
        url: "img/fuji.jpg"
      },
      {
        name: "gala",
        url: "img/gala.jpg"
      }      
    ];

This is our apple database. :-) Or to be more correct REST API endpoint-substitute which provides us with names and images URLs of the apples (data models).

Note: This mock dataset can be easily substituted by assigning REST API endpoints of your back-end to url properties in Backbone.js Collections and/or Models.

Now to make the User Experience (UX) a little bit better, we can add a new route to the routes object in Backbone Route:

    ...  
    routes: {
      '': 'home',
      'apples/:appleName': 'loadApple'
    },
    ...

This will allow users to go to index.html#apples/SOMENAME and expect to see the information about an apple. This information will be fetched and rendered by loadApple function in the Backbone Router definition:

      loadApple: function(appleName){
        this.appleView.render(appleName);
      }

Have you noticed an appleName variable? It's exactly the same name as in the one the we've used in route. This is how we can access query string parameters (e.g, ?param=value&q=search) in Backbone.

Now we'll need to refactor some more code to create Backbone Collection and populate it with data in our appleData variable, and to pass collection to homeView and appleView, and we do it all in Router constructor method initialize:

      initialize: function(){
        var apples = new Apples();
        apples.reset(appleData);
        this.homeView = new homeView({collection: apples});
        this.appleView = new appleView({collection: apples});
      },

At this point we're pretty much done with the Router class and it should look like this:

    var router = Backbone.Router.extend({
      routes: {
        '': 'home',
        'apples/:appleName': 'loadApple'
      },
      initialize: function(){
        var apples = new Apples();
        apples.reset(appleData);
        this.homeView = new homeView({collection: apples});
        this.appleView = new appleView({collection: apples});
      },
      home: function(){        
        this.homeView.render();
      },
      loadApple: function(appleName){
        this.appleView.render(appleName);
      }
    });

Let's modify our homeView a bit to see the whole database:

    var homeView = Backbone.View.extend({
      el: 'body',
      template: _.template('Apple data: <%= data %>'),
      render: function(){
        this.$el.html(this.template({data: JSON.stringify(this.collection.models)}));
      }
    });

For now we just output JSON object in the browser. This is not user-friendly at all, later we'll improve it by using a list and subviews.

Our apple Backbone Collection is clean and simple:

    var Apples = Backbone.Collection.extend({
    });

Note: Backbone automatically creates models inside of a collection when we use fetch() or reset() functions.

Apple view is not any more complex, it has only two properties: template and render. In a template we want to display figure, img and figcaption tags with specific values. Underscore.js template engine is handy at this:

  var appleView = Backbone.View.extend({
    template: _.template(
          '<figure>\
             <img src="<%= attributes.url%>"/>\
             <figcaption><%= attributes.name %></figcaption>\
           </figure>'),
  ...
  });

To make JavaScript string, which has HTML tags in it, more readable we can use backslash line breaker escape ("") symbol or close strings and concatenate them with plus sign ("+"):

  var appleView = Backbone.View.extend({
    template: _.template(
        '<figure>'+
          +'<img src="<%= attributes.url%>"/>'+
          +'<figcaption><%= attributes.name %></figcaption>'+
        +'</figure>'),
  ...

Please note the '<%=' and '%>' symbols, they are the instructions for Undescore.js to print values in properties url and name of the attributes object.

Finally, we're adding render function to the appleView class:

  render: function(appleName){
    var appleModel = this.collection.where({name:appleName})[0];
    var appleHtml = this.template(appleModel);
    $('body').html(appleHtml);
  }

Right now render function is responsible for both loading the data and rendering it. Later we'll refactor the function to separate these two functionalities into different methods.

The whole app, which is in ga-backbone/collection/index.html folder, looks like this:

<!DOCTYPE>
<html>
<head>
  <script src="jquery.js"></script>
  <script src="underscore.js"></script>
  <script src="backbone.js"></script>

  <script>
   var appleData = [
      {
        name: "fuji",
        url: "img/fuji.jpg"
      },
      {
        name: "gala",
        url: "img/gala.jpg"
      }      
    ];
    var app;
    var router = Backbone.Router.extend({
      routes: {
        "": "home",
        "apples/:appleName": "loadApple"
      },
      initialize: function(){
        var apples = new Apples();
        apples.reset(appleData);
        this.homeView = new homeView({collection: apples});
        this.appleView = new appleView({collection: apples});
      },
      home: function(){        
        this.homeView.render();
      },
      loadApple: function(appleName){
        this.appleView.render(appleName);
      }
    });

    var homeView = Backbone.View.extend({
      el: 'body',
      template: _.template('Apple data: <%= data %>'),
      render: function(){
        this.$el.html(this.template({data: JSON.stringify(this.collection.models)}));
      }
      //TODO subviews
    });

    var Apples = Backbone.Collection.extend({

    });
    var appleView = Backbone.View.extend({
      template: _.template('<figure>\
                              <img src="<%= attributes.url%>"/>\
                              <figcaption><%= attributes.name %></figcaption>\
                            </figure>'),
      //TODO re-write with load apple and event binding
      render: function(appleName){
        var appleModel = this.collection.where({name:appleName})[0];
        var appleHtml = this.template(appleModel);
        $('body').html(appleHtml);
      }
    });
    $(document).ready(function(){
      app = new router;
      Backbone.history.start();      
    })

  </script>
</head>
<body>
  <div></div>
</body>
</html>

Open collections/index.html file in your browser. You should see the data from our "database", i.e., Apple data: [{"name":"fuji","url":"img/fuji.jpg"},{"name":"gala","url":"img/gala.jpg"}]. It's not very user friendly but will do for now (we'll make it prettier in the section on subviews).

Now, let' go to collections/index.html#apples/fuji or collections/index.html#apples/gala in your browser. We expect to see on image with a caption. It's a detailed view of an item which in this case is an apple. Nice work!

Event Binding

In the real life getting data does not happen instantaneously so let's refactor our code. For a better UI/UX, we'll also have to show loading icon (a.k.a., spinner or ajax-loader) to users to make sure that they know that the information is being loaded.

Without Backbone.js we'll have to pass a function that renders HTML as a callback to the data loading function, to make sure rendering function is not executed before we have actual data to display.

It's a good things that we have event binding in Backbone. Therefore, when a user goes to detailed view (apples/:id) we only call function that loads the data. Then with the proper even listeners our view will automagically update itself, when there is a new data (or on a data change, Backbone.js supports multiple and even custom events).

Let's change what we call in the router:

  ...
    loadApple: function(appleName){
      this.appleView.loadApple(appleName);
    }
  ...

Everything else is the same till we get to the appleView class. We'll need to add a constructor or an initialize function. initialize is a special word/property in Backbone.js framework. It's called each time we create an instance of an object, i.e., var someObj = new SomeObject(). We can also pass extra parameters to initialize function, as we did with our views (we passed object with key collection and the value of apples Backbone Collection). Read more on Backbone constructors at http://backbonejs.org/#View-constructor.

  ...
  var appleView = Backbone.View.extend({
    initialize: function(){
      //TODO: create and setup model (aka an apple)
    },
  ...

Great, we have our initialize function. Now we need to create a model which will represent a single apple and set up proper event listeners on the model. We'll use two types of events, change and a custom event called spinner. To do that we are going to use on() function which takes these properties on(event, actions, context), read more about it at at http://backbonejs.org/#Events-on:

  ...
  var appleView = Backbone.View.extend({
      this.model = new (Backbone.Model.extend({}));
      this.model.bind('change', this.render, this);
      this.bind('spinner',this.showSpinner, this);
    },
  ...      

The code above basically boils down to two simple things:

  1. Call render function on this object (appleView) when model changes
  2. Call showSpinner when this object (appleView) event spinner is fired

So far so good, right? But what about spinner, or loading a GIF icon? Let's create a new property in appleView:

  ...
    templateSpinner: '<img src="img/spinner.gif" width="30"/>',
  ...    

Remember loadApple function in the router? This is how we can implement it in appleView:

  ...
  loadApple:function(appleName){
    this.trigger('spinner'); //show spinner GIF image
    var view = this; //we'll need to access that inside of a closure
    setTimeout(function(){ //simulates real time lag when fetching data from the remote server
      view.model.set(view.collection.where({name:appleName})[0].attributes);  
    },1000);    
  },
  ...

The first line will trigger the spinner event (the function for which we still have to write).

Second line is just for scoping issues (so we can use appleView inside of the closure).

setTimeout function is simulating a time lag of a real remote server response. Inside of it we assign attributes of selected model to our view's model by using model.set() function and model.attributes property (which returns properties of a model).

Now we can remove extra code from render function and implement showSpinner function:

  render: function(appleName){
    var appleHtml = this.template(this.model);
    $('body').html(appleHtml);
  },
  showSpinner: function(){
    $('body').html(this.templateSpinner);        
  }
  ...

That's all! Open index.html#apples/gala or index.html#apples/fuji in your browser and enjoy loading animation while waiting for an apple image to load.

The full code of index.html file:

<!DOCTYPE>
<html>
<head>
  <script src="jquery.js"></script>
  <script src="underscore.js"></script>
  <script src="backbone.js"></script>

  <script>
   var appleData = [
      {
        name: "fuji",
        url: "img/fuji.jpg"
      },
      {
        name: "gala",
        url: "img/gala.jpg"
      }      
    ];
    var app;
    var router = Backbone.Router.extend({
      routes: {
        "": "home",
        "apples/:appleName": "loadApple"
      },
      initialize: function(){
        var apples = new Apples();
        apples.reset(appleData);
        this.homeView = new homeView({collection: apples});
        this.appleView = new appleView({collection: apples});
      },
      home: function(){        
        this.homeView.render();
      },
      loadApple: function(appleName){
        this.appleView.loadApple(appleName);

      }
    });

    var homeView = Backbone.View.extend({
      el: 'body',
      template: _.template('Apple data: <%= data %>'),
      render: function(){
        this.$el.html(this.template({data: JSON.stringify(this.collection.models)}));
      }
      //TODO subviews
    });

    var Apples = Backbone.Collection.extend({

    });
    var appleView = Backbone.View.extend({
      initialize: function(){
        this.model = new (Backbone.Model.extend({}));
        this.model.on('change', this.render, this);
        this.on('spinner',this.showSpinner, this);
      },
      template: _.template('<figure>\
                              <img src="<%= attributes.url%>"/>\
                              <figcaption><%= attributes.name %></figcaption>\
                            </figure>'),
      templateSpinner: '<img src="img/spinner.gif" width="30"/>',

      loadApple:function(appleName){
        this.trigger('spinner');
        var view = this; //we'll need to access that inside of a closure
        setTimeout(function(){ //simulates real time lag when fetching data from the remote server
          view.model.set(view.collection.where({name:appleName})[0].attributes);  
        },1000);
        
      },

      render: function(appleName){
        var appleHtml = this.template(this.model);
        $('body').html(appleHtml);
      },
      showSpinner: function(){
        $('body').html(this.templateSpinner);        
      }

    });
    $(document).ready(function(){
      app = new router;
      Backbone.history.start();      
    })

  </script>
</head>
<body>
  <a href="#apples/fuji">fuji</a>
  <div></div>
</body>
</html>

Views and Subviews with Underscore.js

This example is available at https://github.com/azat-co/ga-backbone/tree/master/subview.

Subviews are Backbone Views that are created and used inside of another Backbone View. Subviews concept is a great way to abstract (separate) UI events (e.g., clicks) and templates for similarly structured elements (e.g., apples).

Use case of a Subview might include a row in a table, a list item in a list, a paragraph, a new line, etc.

We'll refactor our home page to show a nice list of apples. Each list item will have an apple name and a buy link with onClick event. Let's start with creating a subview for a single apple with our standard Backbone extend function:

  ...
  var appleItemView = Backbone.View.extend({
    tagName: 'li',
    template: _.template(''
           +'<a href="#apples/<%=name%>" target="_blank">'
          +'<%=name%>'
          +'</a>&nbsp;<a class="add-to-cart" href="#">buy</a>'),
    events: {
      'click .add-to-cart': 'addToCart'
    },
    render: function() {
      this.$el.html(this.template(this.model.attributes));
    },
    addToCart: function(){
      this.model.collection.trigger('addToCart', this.model);
    }
  });
  ...

Now we can populate the object with tagName, template, events, render and addToCart properties/methods.

  ...
  tagName: 'li',
  ...

tagName automatically lets Backbone to create HTML element with the specified tag name, in this case <li> — list item. This will be a representation of a single apple, a row in our list.

  ...
  template: _.template(''
         +'<a href="#apples/<%=name%>" target="_blank">'
        +'<%=name%>'
        +'</a>&nbsp;<a class="add-to-cart" href="#">buy</a>'),
  ...

Template is just a sting with Undescore.js instructions. They are wrapped in <% and %> symbols. <%= simply means print the value. The same code can be written with backslash escapes:

  ...
  template: _.template('\
         <a href="#apples/<%=name%>" target="_blank">\
        <%=name%>\
        </a>&nbsp;<a class="add-to-cart" href="#">buy</a>\
        '),
  ...

Each <li> will have two anchor elements (<a>), links to a detailed apple view (#apples/:appleName) and a buy button. Now we're going to attach an event listener to the buy button:

  ...
  events: {
    'click .add-to-cart': 'addToCart'
  },
  ...

The syntax follows this rule:

event + jQuery element selector: function name

Both, the key and the value (right and left parts separated by the colon) are strings. For example:

'click .add-to-cart': 'addToCart'

or

'click #load-more': 'loadMoreData'

To render each item in the list we''ll use jQuery html() function on this.$el jQuery object which is <li> HTML element based on our tagName attribute:

  ...
  render: function() {
    this.$el.html(this.template(this.model.attributes));
  },
  ...

addToCart will use trigger() function to notify collection that this particular model (apple) is up for the purchase by the user:

  ...
    addToCart: function(){
      this.model.collection.trigger('addToCart', this.model);
    }
  ...

Here is the full code of the ** appleItemView** Backbone View class:

  ...
  var appleItemView = Backbone.View.extend({
    tagName: 'li',
    template: _.template(''
           +'<a href="#apples/<%=name%>" target="_blank">'
          +'<%=name%>'
          +'</a>&nbsp;<a class="add-to-cart" href="#">buy</a>'),
    events: {
      'click .add-to-cart': 'addToCart'
    },
    render: function() {
      this.$el.html(this.template(this.model.attributes));
    },
    addToCart: function(){
      this.model.collection.trigger('addToCart', this.model);
    }
  });
  ...

So far, so good?! But what about the master view which is supposed to render all our items (apples) and provide wrapper container for li HTML elements (<ul> is our wrapper). We need to modify and enhance our homeView.

To begin with, we can add extra properties as string understandable by jQuery as selectors:

  ...
  el: 'body',
  listEl: '.apples-list',
  cartEl: '.cart-box',
  ...

We can use properties from above in the template or just hard-code them (we'll refactor our code later):

  ...
  template: _.template('Apple data: \
    <ul class="apples-list">\
    </ul>\
    <div class="cart-box"></div>'),
  ...

initialize function will be called when homeView is created (new homeView()) and we render our template (with our favorite html function) and attach event listener to the collection (which is a set of apple models):

  ...
    initialize: function() {
      this.$el.html(this.template);
      this.collection.on('addToCart', this.showCart, this);
    },
  ...

The syntax for binding event is covered in the previous section. It boils down to calling showCart function of homeView. In this function we append appleName to the cart (along with a line break, br element):

  ...    
    showCart: function(appleModel) {
      $(this.cartEl).append(appleModel.attributes.name+'<br/>');
    },
  ...

Finally here is our long-waited render function in which we iterate through each model in the collection (each apple), create appleItemView for each apple, create <li> element for each apple and append that elements to view.listEl which is <ul> element with a class apples-list in the DOM:

  ...
  render: function(){
    view = this; 
    //so we can use view inside of closure
    this.collection.each(function(apple){
      var appleSubView = new appleItemView({model:apple}); 
      // create subview with model apple
      appleSubView.render(); 
      // compiles tempalte and single apple data
      $(view.listEl).append(appleSubView.$el);   
      //append jQuery object from single apple to apples-list DOM element
    });
  }
  ...

Let's make sure we didn't miss anything in the homeView Backbone View:

  ...
  var homeView = Backbone.View.extend({
    el: 'body',
    listEl: '.apples-list',
    cartEl: '.cart-box',
    template: _.template('Apple data: \
      <ul class="apples-list">\
      </ul>\
      <div class="cart-box"></div>'),
    initialize: function() {
      this.$el.html(this.template);
      this.collection.on('addToCart', this.showCart, this);
    },
    showCart: function(appleModel) {
      $(this.cartEl).append(appleModel.attributes.name+'<br/>');
    },      
    render: function(){
      view = this; //so we can use view inside of closure
      this.collection.each(function(apple){
        var appleSubView = new appleItemView({model:apple}); // create subview with model apple
        appleSubView.render(); // compiles tempalte and single apple data
        $(view.listEl).append(appleSubView.$el);   //append jQuery object from single apple to apples-list DOM element
      });
    }
  }); 
  ...

You should be able to click on the buy and cart will populate with the apples of your choice. Looking at an individual apple does not require typing its name in the URL address bar of the browser anymore. We can click on the name and it opens a new window with a detailed view.

http://cl.ly/image/3t3C2o351233

By using the subviews we re-used the template for all of the items, attached specific event to each item. Those events are smart enough to pass the information about the model to other objects: views and collections.

Just in case, here is the full code for the subview example, which is also available at subview/index.html:

<!DOCTYPE>
<html>
<head>
  <script src="jquery.js"></script>
  <script src="underscore.js"></script>
  <script src="backbone.js"></script>

  <script>
   var appleData = [
      {
        name: "fuji",
        url: "img/fuji.jpg"
      },
      {
        name: "gala",
        url: "img/gala.jpg"
      }      
    ];
    var app;
    var router = Backbone.Router.extend({
      routes: {
        "": "home",
        "apples/:appleName": "loadApple"
      },
      initialize: function(){
        var apples = new Apples();
        apples.reset(appleData);
        this.homeView = new homeView({collection: apples});
        this.appleView = new appleView({collection: apples});
      },
      home: function(){        
        this.homeView.render();
      },
      loadApple: function(appleName){
        this.appleView.loadApple(appleName);

      }
    });
    var appleItemView = Backbone.View.extend({
      tagName: 'li',
      // template: _.template(''
      //        +'<a href="#apples/<%=name%>" target="_blank">'
      //       +'<%=name%>'
      //       +'</a>&nbsp;<a class="add-to-cart" href="#">buy</a>'),
      template: _.template('\
             <a href="#apples/<%=name%>" target="_blank">\
            <%=name%>\
            </a>&nbsp;<a class="add-to-cart" href="#">buy</a>\
            '),

      events: {
        'click .add-to-cart': 'addToCart'
      },
      render: function() {
        this.$el.html(this.template(this.model.attributes));
      },
      addToCart: function(){
        this.model.collection.trigger('addToCart', this.model);
      }
    });

    var homeView = Backbone.View.extend({
      el: 'body',
      listEl: '.apples-list',
      cartEl: '.cart-box',
      template: _.template('Apple data: \
        <ul class="apples-list">\
        </ul>\
        <div class="cart-box"></div>'),
      initialize: function() {
        this.$el.html(this.template);
        this.collection.on('addToCart', this.showCart, this);
      },
      showCart: function(appleModel) {
        $(this.cartEl).append(appleModel.attributes.name+'<br/>');
      },      
      render: function(){
        view = this; //so we can use view inside of closure
        this.collection.each(function(apple){
          var appleSubView = new appleItemView({model:apple}); // create subview with model apple
          appleSubView.render(); // compiles tempalte and single apple data
          $(view.listEl).append(appleSubView.$el);   //append jQuery object from single apple to apples-list DOM element
        });
      }
    });

    var Apples = Backbone.Collection.extend({
    });

    var appleView = Backbone.View.extend({
      initialize: function(){
        this.model = new (Backbone.Model.extend({}));
        this.model.on('change', this.render, this);
        this.on('spinner',this.showSpinner, this);
      },
      template: _.template('<figure>\
                              <img src="<%= attributes.url%>"/>\
                              <figcaption><%= attributes.name %></figcaption>\
                            </figure>'),
      templateSpinner: '<img src="img/spinner.gif" width="30"/>',
      loadApple:function(appleName){
        this.trigger('spinner');
        var view = this; //we'll need to access that inside of a closure
        setTimeout(function(){ //simulates real time lag when fetching data from the remote server
          view.model.set(view.collection.where({name:appleName})[0].attributes);  
        },1000);        
      },
      render: function(appleName){
        var appleHtml = this.template(this.model);
        $('body').html(appleHtml);
      },
      showSpinner: function(){
        $('body').html(this.templateSpinner);        
      }
    });

    $(document).ready(function(){
      app = new router;
      Backbone.history.start();      
    })

  </script>
</head>
<body>
  <div></div>
</body>
</html>

The link to individual item, e.g., collections/index.html#apples/fuji should work independently, by typing it in the browser address bar, as well.

Refactoring

At this point you are probably wondering what is the benefit of using the framework and still having multiple classes, objects and elements with different functionalities in one single file. This was done for the purpose of Keep it Simple Stupid (KISS).

The bigger is your application the more pain there is in unorganized code base. Let's break down our application into multiple files where each file will be one of this types:

  • view
  • template
  • router
  • collection
  • model

Let write these script include tags into our index.html head:

  <script src="apple-item.view.js"></script>
  <script src="apple-home.view.js"></script>
  <script src="apple.view.js"></script>
  <script src="apples.js"></script>
  <script src="apple-app.js"></script>

The names don't have to follow the convention of dashes and dots as long as it's easy to tell what each file is supposed to do.

Now let's copy our objects/classes into corresponding files.

Our main index.html file should look like this:

<!DOCTYPE>
<html>
<head>
  <script src="jquery.js"></script>
  <script src="underscore.js"></script>
  <script src="backbone.js"></script>

  <script src="apple-item.view.js"></script>
  <script src="apple-home.view.js"></script>
  <script src="apple.view.js"></script>
  <script src="apples.js"></script>
  <script src="apple-app.js"></script>
  
</head>
<body>
  <div></div>
</body>
</html>

Content of apple-item.view.js:

    var appleView = Backbone.View.extend({
      initialize: function(){
        this.model = new (Backbone.Model.extend({}));
        this.model.on('change', this.render, this);
        this.on('spinner',this.showSpinner, this);
      },
      template: _.template('<figure>\
                              <img src="<%= attributes.url%>"/>\
                              <figcaption><%= attributes.name %></figcaption>\
                            </figure>'),
      templateSpinner: '<img src="img/spinner.gif" width="30"/>',

      loadApple:function(appleName){
        this.trigger('spinner');
        var view = this; //we'll need to access that inside of a closure
        setTimeout(function(){ //simulates real time lag when fetching data from the remote server
          view.model.set(view.collection.where({name:appleName})[0].attributes);  
        },1000);
        
      },

      render: function(appleName){
        var appleHtml = this.template(this.model);
        $('body').html(appleHtml);
      },
      showSpinner: function(){
        $('body').html(this.templateSpinner);        
      }

    });

apple-home.view.js:

    

    var homeView = Backbone.View.extend({
      el: 'body',
      listEl: '.apples-list',
      cartEl: '.cart-box',
      template: _.template('Apple data: \
        <ul class="apples-list">\
        </ul>\
        <div class="cart-box"></div>'),
      initialize: function() {
        this.$el.html(this.template);
        this.collection.on('addToCart', this.showCart, this);
      },
      showCart: function(appleModel) {
        $(this.cartEl).append(appleModel.attributes.name+'<br/>');
      },      
      render: function(){
        view = this; //so we can use view inside of closure
        this.collection.each(function(apple){
          var appleSubView = new appleItemView({model:apple}); // create subview with model apple
          appleSubView.render(); // compiles tempalte and single apple data
          $(view.listEl).append(appleSubView.$el);   //append jQuery object from single apple to apples-list DOM element
        });
      }


    });

apple.view.js:

        var appleView = Backbone.View.extend({
      initialize: function(){
        this.model = new (Backbone.Model.extend({}));
        this.model.on('change', this.render, this);
        this.on('spinner',this.showSpinner, this);
      },
      template: _.template('<figure>\
                              <img src="<%= attributes.url%>"/>\
                              <figcaption><%= attributes.name %></figcaption>\
                            </figure>'),
      templateSpinner: '<img src="img/spinner.gif" width="30"/>',

      loadApple:function(appleName){
        this.trigger('spinner');
        var view = this; //we'll need to access that inside of a closure
        setTimeout(function(){ //simulates real time lag when fetching data from the remote server
          view.model.set(view.collection.where({name:appleName})[0].attributes);  
        },1000);
        
      },

      render: function(appleName){
        var appleHtml = this.template(this.model);
        $('body').html(appleHtml);
      },
      showSpinner: function(){
        $('body').html(this.templateSpinner);        
      }

    });

apples.js

 
    var Apples = Backbone.Collection.extend({

    });

apple-app.js :

   var appleData = [
      {
        name: "fuji",
        url: "img/fuji.jpg"
      },
      {
        name: "gala",
        url: "img/gala.jpg"
      }      
    ];
    var app;
    var router = Backbone.Router.extend({
      routes: {
        '': 'home',
        'apples/:appleName': 'loadApple'
      },
      initialize: function(){
        var apples = new Apples();
        apples.reset(appleData);
        this.homeView = new homeView({collection: apples});
        this.appleView = new appleView({collection: apples});
      },
      home: function(){        
        this.homeView.render();
      },
      loadApple: function(appleName){
        this.appleView.loadApple(appleName);

      }
    });

    $(document).ready(function(){
      app = new router;
      Backbone.history.start();      
    })

Now let's try to open the application. It should work exactly the same as in previous example subview.

It's way better, but still far from perfect because we still have html templates directly in JavaScript code. The problem with that is that designers and developers can't work on the same files and any change to the presentation requires a change in the main code base.

We can add a few more include to our index.html file:

  <script src="apple-item.tpl.js"></script>
  <script src="apple-home.tpl.js"></script>
  <script src="apple-spinner.tpl.js"></script>
  <script src="apple.tpl.js"></script>

Usually one Backbone view has one templates but in the case of our appleView (detailed view of an apple in a separate window) we also have loading icon spinner.

The content of the files is just a global variable which is assigned the string value. Later we use these variables in our views when we call Underscore.js helper method _.template().

The file apple-item.tpl.js:

var appleItemTpl = '\
             <a href="#apples/<%=name%>" target="_blank">\
            <%=name%>\
            </a>&nbsp;<a class="add-to-cart" href="#">buy</a>\
            ';

apple-home.tpl.js

var appleHomeTpl = 'Apple data: \
        <ul class="apples-list">\
        </ul>\
        <div class="cart-box"></div>';

apple-spinner.tpl.js

var appleSpinnerTpl = '<img src="img/spinner.gif" width="30"/>';

apple.tpl.js

var appleTpl = '<figure>\
                              <img src="<%= attributes.url%>"/>\
                              <figcaption><%= attributes.name %></figcaption>\
                            </figure>';

Try to start the application now. The full code is under refactor folder.

As you can see in previous example we used global scoped variables (without the keyword window).

Note: Be careful when you introduce a lot of variables into the global namespace (window keyword). There're might be conflicts and other unpredictable consequences. For example, if you wrote an open source library and other developers started using methods and properties directly instead of using the interface, what happens later when you decide to finally remove/deprecate those global leaks? To prevent this properly written libraries and applications use JavaScript closures.

Example of using closure and global variable module definition:

(function() {
  var apple= function() {
  ...//do something useful like return apple object
  };  
  window.Apple = apple;
}())

Or in case we need an access to the app object (which creates a dependency on that object):

(function() {
  var app = this.app; //equivalent of window.appliation in case we need a dependency (app)
  this.apple = function() {
  ...//return apple object/class
  //use app variable
  }
  // eqivalent of window.apple = function(){...};
}())

As you can see we've created the function and called it immediately while also wrapping everything in parenthesis ().

AMD and Require.js For Development

This article does a great job at explaining why AMD is a good thing, WHY AMD?.

Start you local HTTP server, e.g., MAMP.

Let enhance our code by using Require.js library.

Our index.html will look very minimalistic:

<!DOCTYPE>
<html>
<head>
  <script src="jquery.js"></script>
  <script src="underscore.js"></script>
  <script src="backbone.js"></script>
  <script src="require.js"></script>
  <script src="apple-app.js"></script>  

</head>
<body>
  <div></div>
</body>
</html>

We only include libraries and one JavaScript file. This file has the following structure:

require([...],function(...){...});

Or in a more explanatory way:

require([
  'name-of-the-module',
  ...
  'name-of-the-other-module'
  ],function(referenceToModule, ..., referenceToOtherModule){
  ...//some usefull code
  referenceToModule.someMethod();
});

Basically, we tell browser to load files from the array of filenames (first parameter of the require() function) and then pass our modules from that files to the function as variables (second argument of require function). Inside of the main function we can use our module by referencing the variables.

So our apple-app.js will look like this:

  require([
    'apple-item.tpl', //shim, change to test files
    'apple-home.tpl',
    'apple-spinner.tpl',
    'apple.tpl',
    'apple-item.view',
    'apple-home.view',
    'apple.view',
    'apples'  
  ],function(
    appleItemTpl,
    appleHomeTpl,
    appleSpinnerTpl,
    appleTpl,
    appelItemView,
    homeView,
    appleView,
    Apples
    ){
   var appleData = [
      {
        name: "fuji",
        url: "img/fuji.jpg"
      },
      {
        name: "gala",
        url: "img/gala.jpg"
      }      
    ];
    var app;
    var router = Backbone.Router.extend({ //check if need to be required
      routes: {
        '': 'home',
        'apples/:appleName': 'loadApple'
      },
      initialize: function(){
        var apples = new Apples();
        apples.reset(appleData);
        this.homeView = new homeView({collection: apples});
        this.appleView = new appleView({collection: apples});
      },
      home: function(){        
        this.homeView.render();
      },
      loadApple: function(appleName){
        this.appleView.loadApple(appleName);

      }
    });

    $(document).ready(function(){
      app = new router;
      Backbone.history.start();      
    })
});    

We put all the code inside the function, required modules by their filenames and passed modules as parameters to the main function. To define module we need to use define function:

define([...],function(...){...})

The meaning is similar to require function. We put dependencies as string of filenames (and paths) in the array and pass it as the first argument. Main function takes dependencies as parameters (the order of parameters and modules in the array is important):

define(['name-of-the-module'],function(nameOfModule){
  var b = nameOfModule.render();
  return b; 
})

Note: there is no need to append .js to the filename. Require.js does it automatically.

Let's start with the templates and convert them into Require.js modules.

This is apple-item.tpl.js:

define(function() {
  return '\
             <a href="#apples/<%=name%>" target="_blank">\
            <%=name%>\
            </a>&nbsp;<a class="add-to-cart" href="#">buy</a>\
            '
});   

apple-home.tpl:

define(function(){ 
  return 'Apple data: \
        <ul class="apples-list">\
        </ul>\
        <div class="cart-box"></div>';
});

apple-spinner.tpl.js

define(function(){
  return '<img src="img/spinner.gif" width="30"/>';
});

apple.tpl.js

define(function(){
  return '<figure>\
                              <img src="<%= attributes.url%>"/>\
                              <figcaption><%= attributes.name %></figcaption>\
                            </figure>';
 });                           

apple-item.view.js:

define(function() {
  return '\
             <a href="#apples/<%=name%>" target="_blank">\
            <%=name%>\
            </a>&nbsp;<a class="add-to-cart" href="#">buy</a>\
            '
});   

apple-home.view.js:

define(['apple-home.tpl','apple-item.view'],function(appleHomeTpl,appleItemView){
return  Backbone.View.extend({
      el: 'body',
      listEl: '.apples-list',
      cartEl: '.cart-box',
      template: _.template(appleHomeTpl),
      initialize: function() {
        this.$el.html(this.template);
        this.collection.on('addToCart', this.showCart, this);
      },
      showCart: function(appleModel) {
        $(this.cartEl).append(appleModel.attributes.name+'<br/>');
      },      
      render: function(){
        view = this; //so we can use view inside of closure
        this.collection.each(function(apple){
          var appleSubView = new appleItemView({model:apple}); // create subview with model apple
          appleSubView.render(); // compiles tempalte and single apple data
          $(view.listEl).append(appleSubView.$el);   //append jQuery object from single apple to apples-list DOM element
        });
      }
    });
})

apple.view.js:

define([    
  'apple.tpl',
  'apple-spinner.tpl'
],function(appleTpl,appleSpinnerTpl){
  return  Backbone.View.extend({
    initialize: function(){
      this.model = new (Backbone.Model.extend({}));
      this.model.on('change', this.render, this);
      this.on('spinner',this.showSpinner, this);
    },
    template: _.template(appleTpl),
    templateSpinner: appleSpinnerTpl,
    loadApple:function(appleName){
      this.trigger('spinner');
      var view = this; //we'll need to access that inside of a closure
      setTimeout(function(){ //simulates real time lag when fetching data from the remote server
        view.model.set(view.collection.where({name:appleName})[0].attributes);  
      },1000);      
    },
    render: function(appleName){
      var appleHtml = this.template(this.model);
      $('body').html(appleHtml);
    },
    showSpinner: function(){
      $('body').html(this.templateSpinner);        
    }
  });
});

apples.js

define(function(){
    return Backbone.Collection.extend({})
});   

I hope you can see the pattern by now. All our code is split into separate files based on the logic (e.g., view class, collection class, template). Main file loads all the dependencies with the require function. If we need some module in non-main file, then we can ask for it in define function. Usually in modules we want to return an object, e.g., in templates we return string and in views we return Backbone View class.

Try launching the example under amd/index.html folder. Visually there shouldn't be any changes. If you open Network tab in Developers Tool, you can see the difference in how files are loaded.

amd/index.html: http://cl.ly/image/2W0I0O1q0V23 refactor/index.html: http://cl.ly/image/122X3q1M0P17

Require.js has a lot of configuration options which are defined through requirejs.config() call in the top level HTML page. More information can be found at http://requirejs.org/docs/api.html#config.

Let's add bust to our example. Bust argument will be appended to the url of each file preventing browser from caching the files. Perfect for development and terrible for production. :-)

Add this to the apple-app.js file infront of everything else:

requirejs.config({
  urlArgs: "bust=" +  (new Date()).getTime()
}); 
require([
...

Network tab will look like this: http://cl.ly/image/3W3U3v3D3p1t. Please notice that each file has 200 instead of 304 (not modified).

Require.js for Production

We'll used Node Package Manager (NPM) to install requirejs in your project folder. In your project folder run these commands in a terminal:

$ npm install requirejs

Or add -g for global installation:

$ npm install -g requirejs

Create file app.build.js:

({
    appDir: "./js",
    baseUrl: "./",
    dir: "build",
    modules: [
        {
            name: "apple-app"
        }
    ]
})

Move the script files under js folder (appDir). Builded files will be places under build folder (dir).

For more information on the build file check out this example: https://github.com/jrburke/r.js/blob/master/build/example.build.js.

Now everything should be ready for building one gigantic JavaScript file which will have all our dependencies/modules:

$ r.js -o app.build.js

or

$ node_modules/requirejs/bin/r.js -o app.build.js

You should get list of files: http://cl.ly/image/4123273V1806.

Open index.html in build folder. The network tab shows much improvement with just one file to load: http://cl.ly/image/3l423v1C3o3r.

For more information, checkout official documentation at http://requirejs.org/docs/optimization.html.

Example code is available under r and r/build folders.

For uglification of JS files (decreases the files sizes) we can use Uglify2 module. To install it with NPM use:

$ npm install uglify-js

Then update app.build.js file with optimize: "uglify2" property:

({
    appDir: "./js",
    baseUrl: "./",
    dir: "build",
    optimize: "uglify2",
    modules: [
        {
            name: "apple-app"
        }
    ]
})

Run r.js with:

$ node_modules/requirejs/bin/r.js -o app.build.js

Your should get something like this:

define("apple-item.tpl",[],function(){return'             <a href="#apples/<%=name%>" target="_blank">            <%=name%>            </a>&nbsp;<a class="add-to-cart" href="#">buy</a>            '}),define("apple-home.tpl",[],function(){return'Apple data:         <ul class="apples-list">        </ul>        <div class="cart-box"></div>'}),define("apple-spinner.tpl",[],function(){return'<img src="img/spinner.gif" width="30"/>'}),define("apple.tpl",[],function(){return'<figure>                              <img src="<%= attributes.url%>"/>                              <figcaption><%= attributes.name %></figcaption>                            </figure>'}),define("apple-item.view",["apple-item.tpl"],function(e){return Backbone.View.extend({tagName:"li",template:_.template(e),events:{"click .add-to-cart":"addToCart"},render:function(){this.$el.html(this.template(this.model.attributes))},addToCart:function(){this.model.collection.trigger("addToCart",this.model)}})}),define("apple-home.view",["apple-home.tpl","apple-item.view"],function(e,t){return Backbone.View.extend({el:"body",listEl:".apples-list",cartEl:".cart-box",template:_.template(e),initialize:function(){this.$el.html(this.template),this.collection.on("addToCart",this.showCart,this)},showCart:function(e){$(this.cartEl).append(e.attributes.name+"<br/>")},render:function(){view=this,this.collection.each(function(e){var i=new t({model:e});i.render(),$(view.listEl).append(i.$el)})}})}),define("apple.view",["apple.tpl","apple-spinner.tpl"],function(e,t){return Backbone.View.extend({initialize:function(){this.model=new(Backbone.Model.extend({})),this.model.on("change",this.render,this),this.on("spinner",this.showSpinner,this)},template:_.template(e),templateSpinner:t,loadApple:function(e){this.trigger("spinner");var t=this;setTimeout(function(){t.model.set(t.collection.where({name:e})[0].attributes)},1e3)},render:function(){var e=this.template(this.model);$("body").html(e)},showSpinner:function(){$("body").html(this.templateSpinner)}})}),define("apples",[],function(){return Backbone.Collection.extend({})}),requirejs.config({urlArgs:"bust="+(new Date).getTime()}),require(["apple-item.tpl","apple-home.tpl","apple-spinner.tpl","apple.tpl","apple-item.view","apple-home.view","apple.view","apples"],function(e,t,i,n,a,l,p,o){var r,s=[{name:"fuji",url:"img/fuji.jpg"},{name:"gala",url:"img/gala.jpg"}],c=Backbone.Router.extend({routes:{"":"home","apples/:appleName":"loadApple"},initialize:function(){var e=new o;e.reset(s),this.homeView=new l({collection:e}),this.appleView=new p({collection:e})},home:function(){this.homeView.render()},loadApple:function(e){this.appleView.loadApple(e)}});$(document).ready(function(){r=new c,Backbone.history.start()})}),define("apple-app",function(){});

Note: The files is not wrapped (everything is on one line) on purpose to show how Uglify2 works. Also variable names are shortened.

Super Simple Backbone Starter Kit

To jump start your Backbone.js development consider using Super Simple Backbone Starter Kit or similar projects:

Releases

No releases published

Packages

No packages published