Tutorial: a full Backbone.Marionette application (cont'd)

Posted on May 13, 2012

 This article is part of a series on writing a complete Backbone.Marionette app.

In the previous post, we started writing our application (see it live!) and now have a functional app, even if it still is somewhat basic.

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.

Displaying error messages

Let’s display error messages depending on the search results. For that, we’ll need to add a method to our BookListView (code):

  
  
    showMessage: function(message) {
    this.$('.books').html('<h1>' + message + '</h1>');
}
  
  

There are 3 cases we want to communicate with our user:

  • there was an error during the search
  • the user didn’t enter a search term
  • there were no results for that search term.

So let’s create event listeners for those in our BookListView:

  
  
    initialize: function() {
    _.bindAll(this, "showMessage");
    var self = this;
    MyApp.vent.on("search:error", function () { self.showMessage("Error, please retry later :s") });
    MyApp.vent.on("search:noSearchTerm", function () { self.showMessage("Hummmm, can do better :)") });
    MyApp.vent.on("search:noResults", function () { self.showMessage("No books found") });
}
  
  

Now, we need to trigger these events in the appropriate cases. The error is already taken care of in the book collection’s “search” event, so we’ve got 2 left. To trigger the “no results” event, we need to modify our book collection’s search function:

  
  
    search: function(searchTerm) {
    this.page = 0;

    var self = this;
    this.fetchBooks(searchTerm, function (books) {
        if (books.length < 1) {
            MyApp.vent.trigger("search:noResults");
        }
        else {
            self.reset(books);
        }
    });
}
  
  

And to trigger the “no search term” event, we’ll need to change our search function, but this time with our search view:

  
  
    if (searchTerm.length < 0) {
    MyApp.vent.trigger("search:term", searchTerm);
}
else {
    MyApp.vent.trigger("search:noSearchTerm");
}
  
  

Infinite scroll

Next feature: loading more books as the user scrolls down. The first step is to monitor a scroll event on our BookListView, and if the user scrolls within 100 pixels from the end of the list, we’ll trigger a special event (code):

  
  
    events: {
    'scroll': 'loadMoreBooks'
},

loadMoreBooks: function() {
    var totalHeight = this.$('> div').height(),
        scrollTop = this.$el.scrollTop() + this.$el.height(),
        margin = 200;

    // if we are closer than 'margin' to the end of the content,
    // load more books
    if (scrollTop + margin >= totalHeight) {
        MyApp.vent.trigger("search:more");
    }
}
  
  

And to test, we’ll listen for the event in our books collection, since that’s where we’d need to load more book from:

  
  
    MyApp.vent.on("search:more",
    function () { console.log("Need to load more books!"); });
  
  

Now let’s make it fully functional. The first thing we need to do is remember the search term for each search, so that we can just fetch it from memory if we want to load more results (see the code).

Then, we need to write a function that will load more books (if available), and add those books to our book collection (code):

  
  
    moreBooks: function() {
    // if we've loaded all the books for this search,
    // there are no more to load !
    if (this.length >= this.totalItems) {
        return true;
    }

    var self = this;
    this.fetchBooks(this.previousSearch,
        function (books) { self.add(books); });
}
  
  

Then, we simply use that function in our event listener:

  
  
    MyApp.vent.on("search:more", function () { self.moreBooks(); });
  
  

The nice thing about how we implemented our search is that the spinner will automatically be shown/hidden as more results are fetched. In addition, Marionette will take care of rendering the new book in our collection. Just sit back and relax!

Modal views: book detail

We’d like our user to be able to click on a book a see detailed information in a modal view. To achieve this, we’ll essentially follow Marionette’s author’s advice in his blog post. Read Derick’s article, then look at the code to see how it was applied to our app.

One thing you’ll see is that the DOM element we use to house modal dialogs is configured as a region on the main application. That is quite logical, as modal dialogs can be used from anywhere within our main app, and we’ll have only one modal open at any one time.

UPDATE: after updating the libraries, the event listener in the ModalRegion constructor should be “show”, not “view:show”. See this code commit.

Adding routing

To link URLs to application state, we’ll need a router. Routers tie URLs to functions in a controller object, which allows us to set the application state properly (code):

  
  
    MyApp.LibraryRouting = function () {
    var LibraryRouting = {};

    LibraryRouting.Router = Backbone.Marionette.AppRouter.extend({
        appRoutes: {
            '': 'defaultSearch',
            'search/:searchTerm': 'search'
        }
    });

    MyApp.addInitializer(function () {
        LibraryRouting.router = new LibraryRouting.Router({
            controller: MyApp.LibraryApp
        });

        MyApp.vent.trigger('routing:started');
    });

    return LibraryRouting;
}();
  
  

Here, we configure the routes within the “appRoutes” object and link them to functions on a controller. This controller is specified in the app initializer, where we instantiate a new router. Once the router is ready, we trigger an event to indicate that routing is ready.

So like our code states, we’ll be calling the “defaultSearch” and “search” methods on MyApp.LibraryApp. We better define those methods now:

  
  
    LibraryApp.search = function (term) {
    LibraryApp.initializeLayout();
    MyApp.LibraryApp.BookList.showBooks(LibraryApp.Books);

    MyApp.vent.trigger("search:term", term);
};

LibraryApp.defaultSearch = function () {
    LibraryApp.search("Neuromarketing");
};
  
  

As we now have a “defaultSearch” action that will be called by default, triggering the initializeLayout function and searching for “Neuromarketing”, we no longer need that code in another initializer and we’ve removed it.

To get our routing to work, we just need one last piece in our puzzle: Backbone.history. We’ll start it in our app.js file:

  
  
    MyApp.vent.on("routing:started", function () {
    if (!Backbone.History.started) Backbone.history.start();
});
  
  

Notice how we wait for the “routing:started” event before starting Backbone.history. That’s because Backbone.js’ history functionality requires at least one router to be active.

Try it out! If you navigate to the index page, you’ll see the app search for “Neuromarketing”. And if you type in “#search/testing” in the address after “index.html”, you’ll see the app search for “testing”. It’ll even show up within the search bar. Pretty cool, no?

One thing is missing, though: if we enter “science” in the search bar and hit enter, the app will search for “science” but we’ll still have the “#search/testing” URL. Let’s fix that by adding to our router (code):

  
  
    MyApp.vent.on("search:term", function (searchTerm) {
    Backbone.history.navigate("search/" + searchTerm);
});
  
  

Backbone.history.navigate will change the app path (everything after “index.html”) to the argument we give it. So what we’re saying here, is that any time somebody searches for something (thereby triggering the “search:term” event), we want to have “search/” followed by their search term in the URL.

Now, users can search and bookmark or share the results: if they reload the URL, they will automatically trigger the “search” method on our controller and load up the proper search results.

A new sub app

With the way we structured our app, it’s pretty easy to create a new sub app with which to replace our library sub app. I’ve created a tiny app (that only displays a simple message) in this commit. You’ll notice I just put the entire app into a single file (and object), which works fine.

Now if you click on the “menu” thing on the left, you go go back and forth between the two sub applications.

But there’s still something that’s a little annoying: if you search for something, click “close”, then go back to “books”, the “Neuromarketing” search is displayed regardless of your previous search. Let’s fix that by slightly changing our defaultSearch (code):

  
  
    LibraryApp.defaultSearch = function () {
    LibraryApp.search(LibraryApp.Books.previousSearch
        || "Neuromarketing");
};
  
  

Now, we’ll try to search for the last searched term in our default search. If that’s empty, we’ll use “Neuromarketing” instead.

And that concludes this Backbone.js and Backbone.Marionette tutorial. At this point, you should be well on your way to writing cool Marionette apps!

 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.