Wednesday, April 30, 2014

Writing a small application using Parse and Backbone - Part 2

Last time

In the previous post I created a Backbone application and integrated it with Parse. I also managed to log the user in using their Facebook account. This time I'm going to create a couple of pages, which will require some understanding of how Backbone's views work.

I managed to figure this out yesterday, so in this post I will just record what worked, along with a few of the problems I recall having encountered.

Routes

First I'm going to add a route called invites. Backbone's routing system is simple enough to get started with, you create a Router object and specify your URL patterns and their handlers. My first handler isn't going to do much; here is the code:

 Invite.Router = Backbone.Router.extend({  
      routes: {  
           "invites"          : "allInvites",  
      },  
      allInvites : function() {  
           console.log("allInvites");  
      }  
 });  

When the user navigates to /#invites, the handler allInvites is called. If you don't have the #, this won't work! The router only works on whatever comes after it, and it needs that starting point. This is not specific to Backbone, and if you Google "fragment identifier URI" you will learn more. I plan to do that one day myself.

The next route I added is to look at the details of a specific invite. This route takes a parameter, the invite's id.

 Invite.Router = Backbone.Router.extend({  
      routes: {  
           "invites"         : "allInvites",  
           "invite/:id"      : "viewInvite"  
      },  
      allInvites : function() {  
           console.log("allInvites");  
      },  
      viewInvite : function(inviteId) {  
           console.log("viewInvite, id=" + inviteId);  
     }  
 });  

You can see how the parameter is specified in the URL, and you will also notice that the parameter name in the URL doesn't need to match the parameter to the handling function.

Now let's make the handlers do something.

Views

To create a view in Backbone, you run Backbone.View.extend passing it a configuration object. This object could contain a function called initialize, which will be called without parameters when the view is created. In order for the view to do anything i.e. modify the HTML of a DOM element, you probably want to add a function which you can call when you're ready, and typically this is called render. But note that you have to call this function yourself, Backbone doesn't call it for you.

After five minutes I realized that setting the HTML of a DOM element is quite easily the most painful experience in the world, and why don't they have a template mechanism, integrating with things like Handlebars? If I knew more about how Handlebars worked, I could have probably figured out how to plug it in, but as it happens, Yeoman came to my rescue again. I ran yo backbone:view AllInvites and it generated this nice file for me:

 Invite.Views = Invite.Views || {};  
 (function () {  
   'use strict';  
   Invite.Views.AllInvites = Backbone.View.extend({  
     template: JST['app/scripts/templates/all_invites.ejs'],  
     tagName: 'div',  
     id: '',  
     className: '',  
     events: {},  
     initialize: function () {  
       this.listenTo(this.model, 'change', this.render);  
     },  
     render: function () {  
       this.$el.html(this.template(this.model.toJSON()));  
     }  
   });  
 })();  

Here's what I noticed:
  • The initialize function sets up the render function to list to the change event on the model
  • (There's something called a model)
  • The render function takes the model and runs it through a templating mechanism, the output of which is HTML which is set to a DOM element called $el. According to this helpful site, the el is a div which is created automatically by Backbone if we don't specify it ourselves.
  • The template seems to be in a file with an EJS extension.
So it looks like I need a model. This model object receives (generates?) a change event, and also has a JSON serialization method. So it can't be any old object, it's probably a Backbone.Model. No problem, I can look that up, and generate one using yo backbone:model once I'm ready.

But what is EJS...oh here it is. With example usage! I love the Internet. It doesn't look at pretty as Handlebars, but I don't care, it will get my work done.

 <h2>AllInvitesView</h2>  
 <ul>  
 <%   
           _.each(this.model.models, function(invite) {  
 %>  
   <li>  
     <a href="#/invite/<%= invite.get('id') %>">Detailed View</a>   
 <%=  
   invite.get('what') + ' ' + invite.get('who') + " " + invite.get('when') + " " + invite.get('where').venueName  
 %>  
   </li>  
 <%  
   });  
 %>  
 </ul>  
 <a href="#">Home</a>  

There's really only one thing to pay attention to: if you want to run JavaScript, enclose it in <% %>, but if you actually want the results to be inserted into the HTML, you have to use <%= %>.

Notice that the data is coming from this.model.models. I actually just stumbled upon it (with a little help from the Chrome developer console), and it worked . A little more exploring led me to understand that this.model refers to a collection (if the view is set up correctly), and the collection provides access to its objects via models. More on this below.

It's time to talk about the models now.

Models and Collections

Running the Yeoman generator yo backbone:model gave me a very simple file, which I haven't needed to touch yet. I ran it to create a model called Invite.Models.Invite, and left it there. But there is a separate generator for collections - yo backbone:collection - and that's what I want right now, since I'm working on a view which displays a collection.

The backbone collection definition looks like this:

 Invite.Collections = Invite.Collections || {};  
 (function () {  
   'use strict';  
   Invite.Collections.AllInvites = Backbone.Collection.extend({  
     model: Invite.Models.Invite  
   });  
 })();  

You can see that the Invite model is referenced in the configuration object, this clearly tells the collection what it's going to hold (in addition, I inferred that that Backbone collections are homogeneous, this turned out to be incorrect).

Creating objects is easy, as is creating a collection and adding these objects to it. Here is some example code which creates some dummy data for my application:

   var invDate = new Date(2014, 04, 19, 15, 0, 0, 0);  
   var venue = {  
     venueName:'Pizza Hut',   
     venueId: 1  
   };  
   appController.allInvites = new Invite.Collections.AllInvites();  
   appController.allInvites.add(new Invite.Models.Invite({ id: 1, what: 'fun', who: 'rohit', when: invDate, where: venue }));  
   appController.allInvites.add(new Invite.Models.Invite({ id: 2, what: 'fun', who: 'abrar', when: invDate, where: venue }));  
   appController.allInvites.add(new Invite.Models.Invite({ id: 3, what: 'fun', who: 'arun', when: invDate, where: venue }));  

You can see that I have created the collection inside an object called appController (which I will get to soon), and then just called the add method on the new Invite objects. There is no schema for the Invite model, and it accepts numbers, strings, dates and sub-objects with no extra work on my part! These objects can now be found in the models property of the collection, i.e appController.allInvites.models.

Now that we know how to create models and collections, and to insert models into a collection, it's time to link them to our views. I'm going to start by linking the appController.allInvites collection to the AllInvites view created earlier.

The way to associate data to a view is just to set the view's model property to the model or collection you want. So I create my view like this:

 var invitesView = new Invite.Views.AllInvites({model:Invite.appController.allInvites});  

Now when I render the view, the template code will find the individual models and iterate over them in the loop

  _.each(this.model.models, function(invite) {  
   ....  
 });  

The App Controller

I have been referring to an object called , which I haven't talked about too much yet. I'm using it as a global state holder and data store, and as I go forward I will figure out what else I want it to do. But I wanted to mention one piece of state it holds - the current view.

I lifted this from this StackOverflow answer, it performs some cleanup on any current view before switching to the new view and calling render. Then it takes the current view's el HTML and assigns it to a div I created in the index.html.

 function AppController() {  
   this.showView = function(view) {  
     if (this.currentView){  
       this.currentView.close();  
     }  
     this.currentView = view;  
     this.currentView.render();  
     $("#targetElement").html(this.currentView.el);  
   };  
 }  
 Invite.appController = new AppController();  

The way I use this is I set up the view in each route handler, and ask the appController to show it.

Next time

That's it for this post. In my next post I'm going to figure out how to use Parse's models and collections.