Tutorial: a full Backbone.Marionette application
This article is part of a series on writing a complete Backbone.Marionette app.
In previous posts, I introduced you to Backbone.Marionette which provides many niceties to help you build complex Backbone.js apps. We’ll cover more advanced topics here, such as Backbone.History, modal windows, organizing code into sub applications, and more. The resulting app can bee seen live here. Let’s get started!
Update (June 2013): I’ve written a book on Marionette. It guides you through developing a complete Marionette application, and will teach you how to structure your code so large apps remain manageable. Also, since this tutorial was written, Marionette has evolved with new functionality and better techniques to write scalable javascript apps. These are naturally covered in the book.
Today, we’re going to rewrite and expand Atinux’s “Backbone Books” example with Marionette. Even if you don’t read French, check out his demonstrations section for backbone “toy apps” to learn from. Atinux was kind enough to grant me permission to rewrite his app and blog about it, but keep in mind that all graphic design and CSS elements are his…
Since we’ve got a log of ground to cover, this post is going to go faster and have less hand holding as we go along. If you read the introduction to Marionette that I wrote, you should be comfortable enough to follow along. You can always refer to the full source code, but more often than not you’ll need to view the linked diffs to see what was written at each step (not all code is copied over here).
Getting started
We’ll start with this code base and just like last time, we’re simply going to create a Marionette application that is going to live within the “#content” DOM element (code). But this time, we’re going to structure our code so that the app we write will actually be a “sub application” that can be removed and replaced with another set of functionality (e.g. if the user clicks on a menu link).
How are we going to manage this? Essentially, we are going to namespace everything: instead of calling our collection “Books” and putting it in the global scope, we’ll call it “MyApp.LibraryApp.Books”, where “LibraryApp” is the name of our sub application.
The downside is that we’d always have to prefix everything with “MyApp.LibraryApp”, which will become annoying really fast… So the trick we’ll use is to leverage an Immediately-invoked Function Expression (IIFE), like so:
MyApp.LibraryApp = function () {
var LibraryApp = {};
return LibraryApp;
}();
We’re declaring “MyApp.LibraryApp” as a function that gets called immediately (notice the parentheses after the function definition). Basically,we’re doing something like this:
var myFunction = function () {
var LibraryApp = {};
return LibraryApp;
}
MyApp.LibraryApp = myFunction();
So what actually takes place within this magical IIFE? Well, currently very little is happening: we simply declare an empty object, and then we return it. Why bother? It allows us to declare “public” and “private members, like so:
MyApp.LibraryApp = function () {
var LibraryApp = {};
LibraryApp.alert = function (message) {
alert(message);
};
LibraryApp.privateAlert = function (message) {
privateMessage(message);
};
var privateMessage = function (message) {
alert('private: ' + message);
};
return LibraryApp;
}();
Any function attached to the LibraryApp object that gets returned by our IIFE will be callable from anywhere within the main application using
MyApp.LibraryApp.functionName
.
So within our code, we’ll be able to call
MyApp.LibraryApp.alert('My message');
and
MyApp.LibraryApp.privateAlert('My message');
. Both will display alerts. However, if we call
MyApp.LibraryApp.privateMessage('My message');
we’ll get an error, since it’s essentially a private function that can only be called within LibraryApp. Voilà, easy encapsulation!
Layouts
Before we get to coding, let’s think about our application. If you look at the app, you can see 2 major areas: a search area, and a list of books corresponding to the search criteria.
To keep these areas manageable (showing views, etc.), while being able to completely remove the sub application and avoid memory leaks, our sub application will be using a Marionette layout. Layouts basically work just like the regions we used in the previous tutorial: you define areas within DOM elements, and then show or close views in those areas.
Here it goes (code):
MyApp.LibraryApp = function () {
var LibraryApp = {};
var Layout = Backbone.Marionette.Layout.extend({
template: "#library-layout",
regions: {
search: "#searchBar",
books: "#bookContainer"
}
});
LibraryApp.initializeLayout = function () {
LibraryApp.layout = new Layout();
LibraryApp.layout.on("show", function () {
MyApp.vent.trigger("layout:rendered");
});
MyApp.content.show(MyApp.LibraryApp.layout);
};
return LibraryApp;
}();
MyApp.addInitializer(function () {
MyApp.LibraryApp.initializeLayout();
});
First, we declare a layout with both regions we talked about above. Then, we declare a function to initialize and show the layout:
- it creates a new layout instance;
- when the layout triggers the “show” event, we trigger another event-wide event that we’ll use later on (in the current code, we simply created a basic listener to check our code is working properly);
- it displays the layout within the overall application’s “content” region.
Then, outside our LibraryApp, we declare an initializer to call this function. Note how our encapsulation works: Layout is private and can’t be called from outside the object, but we can call
MyApp.LibraryApp.initializeLayout()
without any trouble.
Getting books
Our app will need books will need some books to display, so let’s fetch some (code):
var Book = Backbone.Model.extend();
var Books = Backbone.Collection.extend({
model: Book,
initialize: function () {
var self = this;
// the number of books we fetch each time
this.maxResults = 40;
// the results "page" we last fetched
this.page = 0;
// flags whether the collection is currently in the process of
// fetching more results from the API (to avoid multiple
// simultaneous calls
this.loading = false;
// the maximum number of results for the previous search
this.totalItems = null;
},
search: function (searchTerm) {
this.page = 0;
var self = this;
this.fetchBooks(searchTerm, function (books) {
console.log(books);
});
},
fetchBooks: function (searchTerm, callback) {
if (this.loading) return true;
this.loading = true;
var self = this;
MyApp.vent.trigger("search:start");
var query = encodeURIComponent(searchTerm) + 'maxResults='
+ this.maxResults + '&startIndex='
+ (this.page * this.maxResults)
+ '&fields=totalItems,items(id,volumeInfo/title,volumeInfo/subtitle,volumeInfo/authors,volumeInfo/publishedDate,volumeInfo/description,volumeInfo/imageLinks)';
$.ajax({
url: 'https://www.googleapis.com/books/v1/volumes',
dataType: 'jsonp',
data: 'q=' + query,
success: function (res) {
MyApp.vent.trigger("search:stop");
if (res.totalItems == 0) {
callback([]);
return [];
}
if (res.items) {
self.page++;
self.totalItems = res.totalItems;
var searchResults = [];
_.each(res.items, function (item) {
var thumbnail = null;
if (item.volumeInfo && item.volumeInfo.imageLinks
&& item.volumeInfo.imageLinks.thumbnail) {
thumbnail = item.volumeInfo.imageLinks.thumbnail;
}
searchResults[searchResults.length] = new Book({
thumbnail: thumbnail,
title: item.volumeInfo.title,
subtitle: item.volumeInfo.subtitle,
description: item.volumeInfo.description,
googleId: item.id
});
});
callback(searchResults);
self.loading = false;
return searchResults;
}
else if (res.error) {
MyApp.vent.trigger("search:error");
self.loading = false;
}
}
});
}
});
You’ll notice we trigger some events in our “fetchBooks” function, to tell the app what is happening.
We’ll also add some code to check everything is working by dumping a search into our trusty console:
MyApp.addInitializer(function () {
LibraryApp.Books.search("Neuromarketing");
});
Now, if you refresh the page, you should see all sorts of books related to “Neuromarketing” getting dumped in the console.
Showing books
Now that we’ve got books on hand, let’s display them by defining views and templates (code):
MyApp.LibraryApp.BookList = function () {
var BookList = {};
var BookView = Backbone.Marionette.ItemView.extend({
template: "#book-template"
});
var BookListView = Backbone.Marionette.CompositeView.extend({
template: "#book-list-template",
id: "bookList",
itemView: BookView,
appendHtml: function (collectionView, itemView) {
collectionView.$(".books").append(itemView.el);
}
});
BookList.showBooks = function (books) {
var bookListView = new BookListView({ collection: books });
MyApp.LibraryApp.layout.books.show(bookListView);
};
return BookList;
}();
MyApp.vent.on("layout:rendered", function () {
MyApp.LibraryApp.BookList.showBooks(MyApp.LibraryApp.Books);
});
UPDATE: Backbone.Marionette’s CompositeView now lets you specify an itemViewContainer, which in many cases can be used instead of appendHtml. For reference, another tutorial I wrote uses the itemViewContainer within a CompositeView.
Looking at the code, you’ll notice we’ve put this into a new file. This is because we’ll use a “controller” object that will take care of processing data and calling the views, and a separate object (MyApp.LibraryApp.BookList) that will take care of managing the views.
To get these views displayed, we’ll need to search for books to add them to the LibraryApp.Books collections, and then call LibraryApp.BookList.showBooks with the collection. In addition, we need to trigger the collection’s “reset” event so that the CollectionView knows it needs to rerender itself when the collection of books changes (see the code).
One thing that’s important to note here, is that we’re using the “layout:rendered” event to trigger showing the display. If we use a basic initializer, we run the risk of the code being run before the layout is ready, resulting in a runtime error.
Attaching the search view
Now let’s create a search view that will fire the search action based on the term we provide. The twist is that our search view is already fully rendered in the template, so all we’re going to do is use Backbone to manage events, and other things. This is the basic concept of progressive enhancement, which Derick Bailey has covered in two blog posts. Since we don’t want to render content that is already present on the page, we’ll simply attach the search view to its layout region (code):
MyApp.LibraryApp.BookList = function () {
var BookList = {};
var BookView = Backbone.Marionette.ItemView.extend({
template: "#book-template"
});
var BookListView = Backbone.Marionette.CompositeView.extend({
template: "#book-list-template",
id: "bookList",
itemView: BookView,
appendHtml: function (collectionView, itemView) {
collectionView.$(".books").append(itemView.el);
}
});
BookList.showBooks = function (books) {
var bookListView = new BookListView({ collection: books });
MyApp.LibraryApp.layout.books.show(bookListView);
};
return BookList;
}();
MyApp.vent.on("layout:rendered", function () {
MyApp.LibraryApp.BookList.showBooks(MyApp.LibraryApp.Books);
});
All we do here, is declare a search view that will output the search term into the console, and then we create an instance and attach it to the view. You’ll notice we’re once again using the “layout:rendered” we’ve set on our main app, before attaching the view to its “search” region.
This last bit is important: by calling “attachView” instead of “show”, we ensure we’re not double rendering an existing view. In passing, notice we’re using the “layout:rendered” event to know when the layout has been rendered, so that we can attach our search view to existing DOM elements.
If you look closely at the code for MyApp.LibraryApp.BookList, you’ll see that we have 2 things that happen on the “layout:rendered” event, one is declared within the BookList object, and one outside. The reason we create a search view from within the BookList object, is that the SearchView is private, and we wouldn’t have access to it from outside the object. The showBooks function, however, is attached to the BookList and is therefore part of the public API, making it callable from outside of the BookList object.
So now, if you enter a search term and hit enter, you’ll see it appear in the console.
Managing searches with events
Displaying the books we search for in the search bar is pretty easy using events (code).
In our search view:
search: function() {
var searchTerm = this.$('#searchTerm').val().trim();
MyApp.vent.trigger("search:term", searchTerm);
}
And in our Books collection:
initialize: function() {
var self = this;
_.bindAll(this, "search");
MyApp.vent.on("search:term", function (term) { self.search(term); });
}
If you now enter “dogs” in the search bar and hit enter, you’ll see different books appear. Great! Our search is working!
However, the spinner just continues spinning forever, which is pretty annoying. Instead, let’s use it to indicate the app is searching for books. For that, we need to display it when a search starts, and hide it as soon as a search ends.
Remember the “search:start” and “search:end” events we defined? Let’s use them for that in our search view (code):
initialize: function() {
var self = this;
var $spinner = self.$('#spinner');
MyApp.vent.on("search:start", function () { $spinner.fadeIn(); });
MyApp.vent.on("search:stop", function () { $spinner.fadeOut(); });
}
Let’s do one last thing before we take a break: right now, our first search doesn’t display anything in the search bar. One easy way to remedy that is for our search view to listen for the “search:term” event within its initializer (code):
MyApp.vent.on("search:term", function (term) {
self.$('#searchTerm').val(term);
});
Then, instead of directly calling the book collection’s search method, we’ll just trigger the event:
MyApp.vent.trigger("search:term", "Neuromarketing");
And that’s it for part 1! See you in part 2
This article is part of a series on writing a complete Backbone.Marionette app.
Would you like to see more Elixir content like this? Sign up to my mailing list so I can gauge how much interest there is in this type of content.