This is part two of a two-part tutorial. If you haven’t already, get up to speed on part one. In the last part, we took a look at getting a Rails app set up with Backbone.js, and introduced some of the modular concepts behind Backbone: models, collections, and views. In this part, we’ll dig in further to templating, as well ashandling user input with Backbone’s
Event module. Finally, we’ll refactor our app and improve the user experience.
Let’s get started.
Step 7: Add Link to Show and Unfeature Button
We’re printing out the names of the featured albums in our
div, and while that’s a great start, it’s not all that “interactive.” Let’s add a link to the
show action of the controller and a button to unfeature the album. Essentially, then, we’ll be writing our template.
Since we want to go to the show page, our url should be
/users/2/albums/1, where the first integer is the user id, and the second is the album id. That’s simple enough to create out of an Underscore template, since both
id are present in the attributes hash in our Backbone model, based on the JSON response from our Rails app. Let’s do that now:
Great! Let’s refresh the page. That should now be a link. If you click on it, it should go to the show page for that album.
Gotcha: Chrome JSON issue
If you’re developing this in Chrome and you go to the show page for the album, then click back, you’ll likely see a string of raw JSON rendered in your browser instead of the index page. That’s because, in Chrome’s opinion, the last request you made was to
albums.json, not the show page, so it’s rendering the JSON string. It’s a perplexing bug, and one I spent a lot of time trying to figure out. If you’re interested, here’s the StackOverflow question I asked and some research around it. If not, the TL;DR is that this tells the browser that two cacheable requests from the same URL with different file extensions should be treated differently. So add this to your
Step 7 (continued)
Okay. Now that we’ve got that pesky Chrome issue out of the way, let’s add a button. We’ll just add a class of ‘unfeature’ so that we can listen to its click event:
×, we’re just adding a non-breaking space (just adding a space wouldn’t create any separation between the words), and a
times symbol, which is essentially an
x, but looks more like the “delete” symbols we’re used to seeing than simply an x.
Note: If this template looks out of control to you, hang in there! We’re going to refactor it.
Now if you refresh, everything’s there, but it just doesn’t look right. I just added some quick CSS to smooth that over a bit:
In order to do this, I renamed my
application.css.scss file to
application.css.sass (because I hate typing curly braces when I don’t have to!) and added a file called
app/assets/stylesheets/albums.css.sass, where I put the above CSS.
Okay. We’re looking at two issues right now. First, our button doesn’t do anything! We’ll take care of that in the next step. But we also have this pretty nasty template just hanging out in our view. We’ll take care of that when we refactor our large app into discrete files. Hang in there.
Step 8: Handle Button’s Click Event
The Backbone.View module mixes in the Backbone.Events module, giving you the ability to bind to a number of preconfigured events in your template. In this case, it’ll probably help to see an illustration.
AlbumItemView, let’s add an
events property. This takes a hash, or a series of key-value pairs in the following format:
So ours is going to look like this:
Thus, when the DOM element (inside of this view’s
el) with the class of
unfeature receives a
click event, the
unfeature method in this object will be called. Let’s write the simplest possible
unfeature method to make sure it’s getting called properly. Your whole view should now look like this:
If you click our unfeature button, you should now see
So now, what should that
unfeature method really do? Well, it should set the
featured property on this view’s model to false and save it, and then remove that
li from the DOM.
Recall that, inside of our list view, we’re passing our model into our item view on instantiation:
So now we always have access to this view’s model inside of the view by calling
this.model, or in CoffeeScript,
@model. We’re going to set its
featured property to false with Backbone’s
set method, which takes a model property and a value. Let’s put that in our
unfeature method now:
findWhere method is just like the
where method we used before, but it just returns the first model that matches the passed-in hash.) Now that we’ve got our model, let’s set its featured property to a variable so we can see if it’s changing. We’ll do this with the complement to
Now click the unfeature button and run
console.log(album.get('featured')); again. You should get
false. But if you refresh the page and log the same property to the console, you’ll get
true. Have you already figured out the problem? That’s right, we’re marking the
featured property as false, but we’re not saving the model! Lucky for us, it turns out that
Backbone.Model offers us a
save method with the same method signature as
set, meaning it takes a hash of values that you want to save on the object. So let’s change
Now refresh and click the unfeature button. Now let’s open up our console and see if that worked:
Now you should see false. If you refresh again, it doesn’t show up! That’s great, but in order to keep testing this, we’ll have to set it back to featured in the rails console. Open a new tab in your terminal in your project directory, and start the console:
Great. Now let’s reset that
Now if we refresh, the album should show up again. This time, we need to remove that
li from the DOM when we click that button. That’s as easy as adding a call to
this.remove() in the view, shortened in CoffeeScript syntax to
Okay. Now refresh and click on the unfeature button. It should remove that
li from the document, and if you refresh, it doesn’t show up! That’s pretty exciting. Everything is working as expected!
Well, almost. Now you’ve got this nagging feeling that when there are no featured albums, it’s just a huge white box. Let’s commit what we have and fix that in the next step.
Step 9: Empty Placeholder Text
In this step, it’ll be helpful to save our console query to reset the
featured property to
true, since we’ll be testing our changes. That way, we can just press the up arrow in the console and return, and it’ll update our model back to the featured state so we can see it in the browser again.
We’re going to need check the number of
featured albums in our view. But there are a couple of ways to do this. We could check the number of
li items in our
#featured div, or we could directly ask our collection how many featured albums it has. Consider that the responsibility of the Backbone view is to update the DOM in response to changes in the data model as well as handle any user interaction. In this case, the albums collection shouldn’t be concerned with the UI state. Thus, if we confine the UI-based logic to our view, it’ll ensure a good separation of concerns.
Because we’ll check for the number of items after we unfeature one, that same
unfeature method seems like a logical place to put that view logic. Let’s try a first draft of the method. In essence, we’re going to want to check the number of
li elements in the
ul with the id of
featured (the same
ul that acts as the
el for the AlbumsListView). If it’s 0, then we should show some placeholder text (by appending it to the parent
div). Here again, CoffeeScript’s English-like syntax shines:
If we go back to our console and set the album back to the featured state (see above) and refresh the page, we’ll see that, as expected, after we click that “unfeature” button, the placeholder text shows up. But we have two new problems: that
unfeature method looks really ugly with a huge line of logic at the bottom, and if we refresh, there is neither a featured album nor placeholder text.
Let’s start by placing that logic in a separate method and calling that after we call
Okay. This looks a little better. That way, the
unfeature method doesn’t have to be concerned with the number of
li items there are; it’s not its responsibility. It simply has to call another method.
In order to do that, we’ll have to place our
setPlaceholder method in the list view, where it more naturally belongs. Go with me here for a minute. Previously, we used
listenTo method in the list view to respond to changes in the album collection. The collection didn’t tell the list view what to do, it just told it that it had synced, and left it to the list view to behave accordingly. That’s all captured here:
We can use a similar technique to pass a message from the item view to the list view. Here, we after we
remove() the item view’s
$el from the DOM when the model is unfeatured, we’ll tell the list view that we’ve done so, and leave it to the list view to check if it needs the placeholder text. Let’s begin by writing the code we’d love to write, and work backward from there. So here’s what I’d love for our unfeature method to look like:
We already had the first two lines. I added the last line to say, when we remove an element, this should trigger a method on the list view to handle the presence or absence of placeholder text.
trigger method allows us to pass the name of a method we want called on an object.
This is great, but we don’t currently have access to the
@listView in the item view; it’s undefined. Thankfully, the process of creating a Backbone view takes an options hash, so we can pass in the list view when we instantiate the item view. Let’s do that now:
Great. You can see (line 5 in the gist above) that in the instantiation of the item view, we’re setting the value of the
listView attribute to
this, which in this case is the instance of the list view. Now we need to tell our list view how to respond to the
handlePlaceholder trigger we’ve specified in the item view. We can do that with
on method, which takes three arguments: the name of the trigger, the method we’re going to call in response, and the value of the
this keyword inside that method. Let’s place this in our
initialize method for the list view:
Here, we’re saying
this being the list view) the
handlePlaceholder trigger, call
this.setPlaceholder, and make the list view the value of
this inside that method. So let’s write that method. Spoiler alert! We already wrote most of it in our item view above:
Fantastic. Now let’s go back to our console and make that album featured again (
Album.where(title: 'A Love Supreme').first.update_attribute(:featured, true)), refresh the page, and click the
unfeature link. We should then see the placeholder text. Awesome! Now let’s refresh and see if it still works…
Bummer. Nothing shows up at all. There are no featured albums and no placeholder text.
That’s a relatively easy problem to solve now that we’ve decoupled our list and item views. Let’s call
setPlaceholder in the initialize method, so it’ll get called when the list view gets instantiated, which happens just after the page load, where we’re currently seeing it blank:
Now make the album featured again and refresh. Well, that’s close. Now we have both the featured album and the placeholder text. So let’s add some logic to our
setPlaceholder method. We’re going to check if there are any featured albums (known by the number of
li elements in our list view’s
el), and set the placeholder text if that’s the case. Otherwise, let’s remove the placeholder text altogether.
Well, now we’re back to the same problem: on page load, there isn’t any placeholder text. Why is that? Well, let’s think about when the list view will know whether or not there are any featured albums. It’s really not until the collection gets the response back from the server, right? Currently, this method is being called before the collection returns. Thus, there are no featured albums to show. Luckily, the
fetch() method we’re calling on the collection takes a
success() callback. This means we can tell it what to do when it returns.
Here, we’re checking the length of the
featured() method in the collection instead of checking the DOM. Now refresh without resetting our album to featured. Great. We have the placeholder text on page load, but if we do set the model back to featured and refresh, we get both the model and the placeholder text again. What if, instead of checking for the number of
li elements in our
$el on page load (which will always be empty on page load, because, again, that’s before the collection returns from the server), we checked for the number of featured albums in our collection before it renders?
Now make sure the album is set to featured and refresh. You should see just the featured album in our featured box, and when you click “unfeature”, the placeholder text should appear. If you refresh, you should just see the placeholder text. You’re done with this step!
Step 10: Refactoring
It feels great to have our app feature complete, doesn’t it? We’ve built our app so that it dynamically displays a series of featured albums that you can unfeature with a single click and without a page reload. It’s fun to play with! But at this point, we should resist the temptation to ship it as is. Why? A few reasons.
Firstly, our app has reached 46 lines, including whitespace. If we add much more to it, it will quickly become larger than we can conveniently fit in our head at once. At that point, maintaining our app will become a cognitive burden.
Secondly (and relatedly), while we’re working in one part of the application, it creates a lot of noise to have to look at every part of it at once. If I just want to work with our albums collection, I shouldn’t also have to look at our albums list view which, while related, again adds to the amount of information I have to keep in my head simultaneously.
Thirdly (and arguably most importantly), if we think about testing this app right now, seeing it as one large file encourages us to think of testing the entire app as one large piece of functionality. Granted, we’re not writing tests in this tutorial (if you’d like to see that in a future post, let me know via email or in the comments), but if we were, it would behoove us to think of our app as discrete components that we can develop and test in isolation from one another. That is, after all, the point of this modular architecture. It’s also how we’re writing our Rails application. If we’ve decided that this style of development is beneficial on the server side, it follows that we’d want to apply that to the client, as well.
Okay, we’ve agreed that we want to break out each component into its own file. Where to start? A good starting point is to take another look at our
Following the load order, the
application.js file itself is going to be loaded before any of the components, which will attach our
App object to the global namespace (which, in the browser is
window) and give us
Views namespaces for those components. It will then include those directories, and finally, our
app file, where we’ll initialize them.
Let’s look at refactoring in the order we’ve laid out in our
application.js file. The first
tree, so to speak, is our
templates directory. This will be the hardest part because it actually requires writing new code. After this, it’s smooth, copy-paste sailing.
We’re going to rewrite the Underscore template from our item view in haml.js. The syntax will look familiar, since the rest of our markup is written in haml. Let’s create a new file called
unfeature. This will make for a very concise haml.js template:
That should look familiar, because it is, in fact, valid haml.
There are a couple more things we have to do to get this rendered in place of our Underscore template, the first of which is something of a “gotcha.” The
backbone_on_rails gem gives us a lot of great things, but by default, it puts the JS templates directory under
app/assets, whereas all the other components, such as models, are placed in
templates directory into
application.js file to require the correct path to the
templates directory. Change this:
By deleting that one period, we’re telling our
application.js file to look for the templates directory not one directory up (which would be
app/assets), but in the current directory. Great. Last step: tell Backbone where to look for the template with the JST object in our item view, still in
Great. Now if we refresh, we should see no changes to the app’s functionality. That’s key with this refactoring. Since we don’t have tests, we’ll have to manually test that the app is still working in between each step. Notice how much cleaner that makes the template.
Since we’re working with the item view, let’s break that into its own file, too. We’re basically going to cut the whole thing and paste it into its own file:
Now refresh. Everything should work, and the JS console should not have any errors.
Okay! We’re well on our way. Now let’s start at the top of our
app.js.coffee file with the model. Here’s now what our
We’re on a roll. Why stop here? Same thing with the collection:
Still no errors in the console? Great. One more:
Great. Now all that should remain in our
app/assets/javscripts/app.js.coffee file is the code we wrote to bootstrap our application on
How does that feel? If you’re like me, and while you were cutting and pasting the code, you kept your text editor windows open, you might feel significantly lighter now that you’re looking at your app in discrete files. Now we know that if we want to work in one part of the application, we don’t have to be distracted by the noise of several other components.
Step 11: Show a spinner while records are fetched
The great thing I notice about refactoring my code is that it makes me feel like improving the interface or experience of the application more palpable. I find when I’m not trying so hard just to get something to “work,” and I’ve written code I consider production quality, I can worry about other details.
In this case, it’s pretty jarring that when you load the page, it pauses for a second, and the featured album abruptly appears. I’d love to show a loading spinner while the records are being retrieved. Luckily, that isn’t difficult.
We’ll place the spinner in the
.featured-albums.panel markup, and on the
success callback from the
fetch() method on the collection, we’ll hide it. Let’s start by choosing a great loading image.
After a small search, I found these excellent and simple pure CSS spinners on GitHub. Let’s use one of those. Let’s also add a header. So now our featured albums panel should look like this:
You can write your own CSS or have a look at the diff in GitHub at the CSS I wrote for the loading image; designing a great loading image isn’t really in the scope of this article.
On the document load, we’re going to tell our list view to hide the loading image before it calls
setPlaceholder(). Let’s take a first stab at it:
Now refresh. Well, it works, but that jQuery
hide() call feels a little out of place. That essentially makes the albums collection manage the DOM in response to its successful request. And if you recall, we’ve been very careful to keep DOM logic out of the collection. That part of the DOM is the domain of the list view. Let’s refactor this by placing the call in there:
You may be asking why we’re returning
this (which is the list view itself). We’re borrowing this technique from our
render() method in our item view. Because we’ve returned
this, when we go back to the
success() callback, we can now chain two method calls off of
Now this is much more declarative, and nicely removes the duplication that would’ve been necessary had those been two separate method calls. The list view is managing its own state in response to the album collection’s
fetch; that way, the collection can do what it does best: reflect the state of the models it’s in charge of.
Excellent. Now we have an attractive loading animation. Even though the user only sees it for a split second, it makes the overall experience much less jarring. Let’s commit our code and take look at the summary of what we’ve done.
Our overall goal was to get a broad introduction to the key concepts of the Backbone.js library: models, collections, views, and templates, and the ways that they communicate via events and callbacks. We also looked deep into the guts of certain parts of the Backbone library. I hope you’ve found, as I have, that the more you dig around under the hood, the less scary it becomes. After all, as of this writing, the entire library weighs in at just 1,736 lines of code, including whitespace. The entire annotated source can be read through in an evening, should you be so inclined.
There are a few topics we didn’t cover. Importantly, we didn’t even look at Backbone’s router because it wasn’t in our feature requirements to, say, make a URL for a certain UI state linkable. If you’re looking to dive more deeply into Backbone, I would recommend digging into the router. And now that you’re familiar with the rest of Backbone, the concepts introduced in the router should follow naturally.
If you’ve gotten this far, thanks for reading, and let me know if there are any Backbone topics you’d like to see in a future tutorial!