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.