Building a web app using Backbone.js and require.js – Part 2

This post is the conclusion to the article Building a web app using Backbone.js and require.js – Part 1.  Please be sure to read that article first before proceeding.

In my previous post, we discussed the original idea for the project, outlined what its supposed to do, and started dissecting the project’s main.js and app.js files. Both of those files frequently referred to the “Games” Backbone view and collection, but never really explained them. In this part of the article, we will take a close look at how those are created, how they relate to each other, and how we handled a lot of the in-between.

Models & Collections

The core of any data driven web application is going to be, obviously, the data!  In our app, our main data model is a game.  Of course, we have a collection of games as well.

Lets take a look at our models/m_game.js file:

Reminder: I’ve give each of my script files a prefix that let me know what kind of file they are in my editor (Visual Studio). Specifically, my models all start with m_ and my views with v_. This really is just so that when I have game.js, games.js, game.js, and games.js all open I don’t lose my mind! Having m_game.js, m_games.js, v_game.js, and v_games.js makes it a little easier to jump around files and know exactly what I’m looking for/at.
define(['config','backbone', 'models/m_user'], function (config, Backbone, User) {
    var Game = Backbone.Model.extend({
        idAttribute: 'gameId',
        url: config.apiRoot + 'games',
        defaults: {
            friends: new User.Collection(),
            userConsoleId: 0,
            detailsLoaded: false
        }
    });
    var Games = Backbone.Collection.extend({
        model: Game,
        url: config.apiRoot + 'games'
    });

    return {
        Model: Game,
        Collection: Games
    };
});

Again, you can see that we are using require.js’s define method.  Here we include our list of dependencies: config (our app’s general configuration settings), backbone, and a User model.  At this point, config is just an object that has a single property “apiRoot” which contains the path to our REST web api url “/api/”.

Our “Game” Backbone model is pretty much a standard Backbone model.  The idAttribute is set manually to match what we will be receiving from the server.  The URL is configured to point to our REST api (this is used when we do fetch() later on).  And we setup a few defaults (just to be safe).

After we define our model, we also declare a collection. Line: 11  Our Games collection just has its model set to our Game model, and again the URL configured for the REST WebAPI url.

Finally we return an anonymous object that just contains the Model and Collection Line 16. This handy tip I saw on someone’s blog (forgive me as I cant recall) and I’ve found that it works much cleaner than separating the collections from the models in their own defines and script files.  I do this with basically all of my models and collections.

Views

If we refer back to our app.js file, we had this chunk of code:

var games = new Game.Collection();
var view = new GamesView({ collection: games });
games.fetch({
    success: function () {
        $('#app').html(view.render().el);

As you can see, we are creating our games collection by creating a new Game.Collection() (Game being our model defined earlier above.). Then we create a new GamesView and initialize its collection with the games collection we just created.  Here is the GamesView:

define(['backbone', 'views/v_game'], function (Backbone, GameView) {
    var View = Backbone.View.extend({
        tagName: 'ul',
        id: 'games',

        render: function () {
            this.$el.html('');
            var that = this;
            _.each(this.collection.models, function (item) {
                that.renderGame(item);
            }, this);

            return this;
        },

        renderGame: function (item) {
            var view = new GameView({ model: item });
            this.$el.append(view.render().el);
        }
    });

    return View;
});

Here we define our Games Backbone view, which (since its a collection) depends on the singular Game view (as referenced by views/v_game) for each item in its collection. This GamesView is pretty basic; its an unordered list, with an id of “games”, and has a standard render method. The render method uses underscore’s _.each to iterate through the collection of games, and render each individually. The “renderGame” method Line: 16 creates a new singular GameView and populates its model with the current item from our collection iteration. It then simply calls that view’s render method, and appends the generated html to the GamesView’s html. Lets take a peek at the singular GameView below:

define(['config', 'backbone', 'jquery.simplemodal', 'views/v_gamedetails'], function (config, Backbone, modal, DetailsView) {
    var View = Backbone.View.extend({
        tagName: 'li',
        template: $('#gameTemplate').html(),
        events: {
            'click':'showDetails'
        },
        render: function () {
            var tmpl = _.template(this.template);
            this.$el.html(tmpl(this.model.toJSON()));
            return this;
        },
        showDetails: function (e) {
            var detailsView = new DetailsView({ model: this.model });
            $('#detailsView').remove();
            $('body').append(detailsView.render().el);
            $.modal.close();
            $('#detailsView').modal({ opacity: 85, overlayClose: true, escClose: true, autoResize: true });
        }
    });

    return View;
});

Now, this GameView is a little more meaty! Since our GamesCollection is basically just a UL, we need a way to handle each of the LIs within it. That’s where our GameView comes into play. The GameView will render a single game. Its dependencies are again pretty basic (note it references a GamesDetails view, which we will see below). GameView uses an underscore template (if you recall earlier in Part 1 of this article, we used Razor’s @RenderPage() methods to insert our templates right into the index.cshtml page) to generate its HTML. It also has an event defined for ‘click’ that will show the details for this game. The render() method for the GameView is super basic, just taking the value of the local template variable (again just the HTML of our template defined in our index.cshtml file), passing it to underscore’s _.template() and storing that in a local variable called tmpl (convention). Then it takes the current view’s model, converts it to JSON, and passes that to the _.template method defined in tmpl.

Important Note: you might have noticed that I always “return this” at the end of my render calls. This is so that we ensure we can continue to use chaining after our method calls in our views. So, for example, instead of: myView.render() forcibly returning the rendered HTML, we can instead do: myView.render().doSomethingElse().ThenDoThis().el (then return el, which is the HTML). This is just good habit.

Our click event handler triggers the “showDetails” function (and since its a standard dom event, we can receive a normal event as its parameter – just like jQuery). Since we only referred to ‘click’ as the trigger, and didn’t specify any additional selectors, the click event will occur on the tag itself, or the LI in this case. Since this view is rendered from a template, that might contain a lot of different HTML elements, we could have easily used something like ‘click a.boxCover’ . Looking at the actual function for showDetails, we can see that we are creating yet another view, a DetailsView, and populating its model with a copy of the same model being used in the GameView (or, in other words, the game data model itself). Since our details are going to display via a modal, the first thing we do is remove any that might have existed previously. Line: 15 Then we render the new DetailsView, appending that rendered HTML to our main body (hint: the DetailsView renders a div with an id of “detailsView” which is why the first thing we did was call a remove on #detailsView). After that we forcibly close any modals that might have been open already (failsafe really), and call our simplemodal plugin to display the newly generated #detailsView!

[toggles]
[toggle title=”View the full Game.cshtml HTML template” state=””]

<script id="gameTemplate" type="text/template">
<a class="game"><img src="/content/assets/covers/<%= gameId %>.jpg" class="gamebox large"></a>
<p class="title"><%= title %></p>
<p><%= moment(releaseDate).format("MMM Do, YYYY") %>  (<%= moment(releaseDate).fromNow() %>)</p>
<p><% for(var c=0; c<consoles.length; c++){ %>
        <span class="console-badge c<%= consoles[c].consoleId%>"><%= consoles[c].abbrev %></span>
    <% } %>
</p>
</script>

[/toggle]
[/toggles]

OK, lets recap where we’re at so far:

  • We have a GamesView view, thats just a UL because its a collection of games
  • The GamesView render() will loop through every item (game) in its collection, and render that specific item via a new GameView
  • The GameView receives a model (game) and renders it as an LI.
  • The LI has a click event that will create a new DetailsView, populating it with its same model (game), rendering and popping it up in a modal window.

Lets now take a look at the Detailsview.

GameDetails View

The DetailsView is probably the most complex view in the project. This is because the DetailsView is made up of a few additional views, each containing their own set of events, and also dynamically loads additional details whenever it is displayed. The DetailsView will display the full details of the Game which it will retrieve via a fetch(). It will also display a list of all of your friends that are getting the game, each clickable to view their additional details.  The “Gettin” button will be rendered from a view, which allows you the option of selecting which console you intend to get this game on (Xbox 360, PS3, etc – whatever the game is available on). Here is the full DetailsView code:

define(['backbone', 'models/m_user', 'views/v_friends', 'views/v_gettinbutton'],
function (Backbone, UserModel, FriendsView, GettinButtonView) {
    var View = Backbone.View.extend({
        tagName: 'div',
        id: "detailsView",
        className: 'hidden',
        template: $('#gameDetailsTemplate').html(),
        render: function () {
            var that = this;
            var tmpl = _.template(this.template);
            this.$el.html(tmpl(this.model.toJSON()));

            if (this.model.get('detailsLoaded') === false) {
                this.loadDetails();
            } else {
                //use nested views to render each section individually:
                this.renderFriends(this.model.get('friends'));
                this.renderGettinButton(this.model);
                $('#gameDetailsLoader').slideUp('fast');
                $('#gameDetailsFull').slideDown('fast');
            }

            return this;
        },
        renderFriends: function (fr) {
            var view = new FriendsView({ collection: fr });
            this.$el.find('#divFriends').html(view.render().el);
        },
        renderGettinButton: function(g){
            var view = new GettinButtonView({ model: this.model });
            this.$el.find('#gettinButton').html(view.render().el);
        },
        loadDetails: function () {
            var friends = new UserModel.Collection();
            //**technically this whole area should be fetching the game details model - not friends
            var that = this;

            friends.fetch({
                success: function () {
                    that.model.set('friends', friends);
                    that.model.set('detailsLoaded', true);
                    that.render();
                }
            });

            return this;
        }
    });

    return View;
});

[toggles]
[toggle title=”View the full GameDetails.cshtml HTML template” state=””]

<script id="gameDetailsTemplate" type="text/template">
    <section id="gameTitle" style="background-image: url(/content/assets/backgrounds/<%= gameId %>.jpg);">
        <h2>&nbsp;<%= title %></h2>
    </section>
    <div style="padding: 1%;">
        <div style="float: right;">
        <% for(var c=0; c<consoles.length; c++){ %>
            <span class="console-badge c<%= consoles[c].consoleId%>"><%= consoles[c].abbrev %></span>
        <% } %>
        </div>
        <b>Release Date:</b> <%= moment(releaseDate).format("MMM Do, YYYY") %> (<%= moment(releaseDate).fromNow()%>)
    </div>

    <div id="gameDetailsLoader" style="text-align: center;">
        <img src="http://assets3.notonthehighstreet.com/images/shared/icons/ajax-loader.gif?1362151400" />
    </div>

    <div id="gameDetailsFull" class="hidden" style="padding: 1%;">
        <div style="background-color: #f5f5f5; padding: 1%; border: solid 1px #c1c1c1;">
            <%= description %>
        </div>
        <br />
        <div style="float: right; width: 49%; padding: 0 1% 0 1%; font-size: .9em; position: relative;">
            <h3>&nbsp;</h3>
            <div id="gettinButton"></div>
        </div>
        <div style="float: left; width: 49%">
            <h3>Preorder Bonuses:</h3>
            <img src="/content/images/vendors/vendor_az.jpg" style="border: solid 1px #353535;" />
            <img src="/content/images/vendors/vendor_bb.jpg" style="border: solid 1px #353535;" />
            <img src="/content/images/vendors/vendor_tr.jpg" style="border: solid 1px #353535;" />
            <img src="/content/images/vendors/vendor_wm.jpg" style="border: solid 1px #353535;" />
            <br />
        </div>

        <div class="clear"></div>

        <br />
        <h3>Friends:</h3>
        <div id="divFriends" style=" height: 60px; overflow-x: scroll;"></div>

        <br />
        <h3>Upcoming Games:</h3>
        <div id="divUpcoming" style=" height: 150px; overflow-x: scroll;"></div>
    </div>
    <div class="clear"></div>
</script>

[/toggle]
[/toggles]

That looks like a lot! But when we break it down by section you’ll realize its not so bad at all.

First, we setup the normal components of any Backbone view; the tagName, HTML id, className, and client template html (using jQuery to grab the html).  This particular view is really all about rendering a bunch of subviews.  The main render() method does a standard underscore template HTML generation.  Then it checks against an internal property of our game model “detailsLoaded”.Line: 14 This property we are using to determine if the additional details for the view have already been retrieved from the server (to cache the data basically).  If detailsLoaded is false, we call an internal method “loadDetails” that creates some additional models, and then calls their fetch (for the purposes of this article, we just setup Friends as its own view and data). If detailsLoaded is true (as it is set to true after loadDetails succeeds), then we simply render the views for Friends, the Gettin button, and then some housekeeping (get rid of the animated gif for the loader and reveal the actual data that was loaded).

When we render our Friends Line: 25, we create a view that is instantiated with the collection of friends (that was just retrieved from the server using loadDetails).  Inside the FriendsView is basic Backbone stuff, but since its a view containing a collection, in its render method we execute an individual FriendView render per item in the collection:

render: function () {
    this.$el.html('');
    var that = this;

    _.each(this.collection.models, function (item) {
        that.renderFriend(item);
    }, this);

    return this;
},

renderFriend: function (item) {
    var view = new FriendView({ model: item });
    this.$el.append(view.render().el);
}

The individual Friend view is pretty basic:

define(['backbone'], function (Backbone) {
    var View = Backbone.View.extend({
        tagName: 'img',
        className: 'boxfriend',
        initialize: function () {
            this.listenTo(this.model, "change", this.render);
        },
        events: {
            'click': 'showDetails'
        },
        render: function () {
            this.$el.attr({ 'src': this.model.get('avatar') });
            this.$el.html(this.el);
            return this;
        },
        showDetails: function () {
            console.log("you clicked on friend: " + this.model.get('username'));
        }
    });
    return View;
});

Again, everything happening here is pretty standard, with the exception of the initialize function. When we initialize our view (for a Friend), we add a listener to detect whenever the model changes, and when it does, execute the view’s render function Line: 6.  This section is important because this is where you can have your views actually bound to your models, so whenever your models change (or updated data is retrieved from the server, for example) the view will immediately react and/or re-render and display the updated information.

The Gettin button view is going to work in a similar way to the Friends list.  It is its own view, and is being rendered after the additional details for the game are loaded. Line: 29 The Gettin button view does something a little differently though:

define(['backbone', 'models/m_console'], function (Backbone, ConsoleModel) {
    var View = Backbone.View.extend({
        tagName: 'div',
        className: 'gettin-button-container',
        initialize: function () {
            this.collection = new ConsoleModel.Collection(this.model.get('consoles'));
        },

        events: {
            'click a.gettin-button': 'showConsoleList'
        },

        render: function () {
            var that = this;
            var consoles = '';
            this.$el.html('<a href="#" class="gettin-button"><span style="float: right;">▼</span>Gettin...</a><div class="consoles-list hidden"></div>');

            _.each(this.collection.models, function (c) {
                that.renderConsoleItem(c);
            });

            return this;
        },
        renderConsoleItem: function (c) {
            var view = new consoleView({ model: c, gameid: this.model.get('gameId') });
            this.$el.find('div.consoles-list').append(view.render().el);
        },
        showConsoleList: function (e) {
            e.preventDefault();
            $('div.consoles-list').toggle();
        }
    });

    var consoleView = Backbone.View.extend({
        className: 'console-item',
        events: { 'click':'clickedConsole' },
        render: function () { this.$el.addClass('c' + this.model.get('consoleId')).html(this.model.get('title')); return this; },
        clickedConsole: function () {
            console.log('Youre gettin it for: ' + this.model.get('title'));
            var that = this;
            $.ajax({
                url: '/api/gettin',
                data: '{ gameid: ' + that.options.gameid + ', consoleid: ' + that.model.get('consoleId') + ' }',
                type: 'POST',
                contentType: 'application/json; charset=utf-8',
                success: function (d) {
                    console.log("Result of gettin button ajax call: " + d.status);
                }
            });

            $('div.consoles-list').hide();
        }
    });

    return View;
});

At first glance, this is yet another basic Backbone view. Although, you can see a few minor things happening that are different.

First, in our initialization Line: 6, we create a new collection of ConsoleModel that we initalize with the consoles property of our model. Every game “record” that we retrieve from the backend contains the basic information for a game, as well as an array of “Consoles” that the game is available on (Xbox 360, PlayStation 3, PC, etc). The way the Gettin button will work, is that its basically going to behave like a regular dropdown list – clicking the button will dropdown a list of consoles that the game is available on and then the user clicks the console that they are intending on purchasing the game on.

The Gettin button will render as a regular div, but inside the render function we manually set some HTML on the fly: first we create a simple A tag, give it a class, some style, the text that should appear, as well as another div that will contain the actual list of consoles. Line: 16

Then we do a standard iteration through the collection of consoles, rendering each individually and appending to the HTML of the div that we just manually inserted after the A tag. However, the item render creates a new “consoleView”. Well, since we aren’t ever going to use the consoleView anywhere but inside this Gettin button, we declared that right below (instead of its own file).Line: 34 Consider it an ad-hoc view (temporary and disposable). That ad-hoc view really only concerns itself with 1 thing, registering a click event and handling that event. Lines: 36, 38

[alert style=”alert-info”] Important Note: You can see in Line: 25 when we created our new view, in addition to the model we also included some additional information; gameid. To access this information in our actual view, we need to refer to this.options. Line: 43[/alert]

Inside the click event for our consoleView, we make a quick (and dirty) ajax call back to our server, passing the gameId and consoleId. The server handles actually persisting this information (under our user account). Then we just hide the list! Typically, we would also reveal some kind of indicator onscreen (a success notice, checkmark icon, fade the gettin button green – something!).

That’s it for the Gettin button!

OK, lets recap what just happened with the game details:

  • When a Game is clicked its GameDetails view is generated, and the div of its details is displayed via a standard jQuery modal window.
  • Inside the GameDetails, a number of different views (areas) are rendered including Friends and a Gettin button.
  • The Friends list is actually retrieved via a fetch() and each Friend can be clicked that will trigger an event.
  • The Gettin button is just a dropdown list of consoles the game is available on, each clickable that sends some basic data back to the server for persistence (via a standard jQuery ajax post).

Summary

In this series of articles, I deconstructed and dissected a pretty typical website application that was created using Backbone.js and Require.js.  The application, although basic, featured a lot of standard UI type stuff that we all deal with on a daily basis.  Even though it looks like a lot of code and at times really seems like overkill, I can say first hand that I now definitely prefer coding a site like this using Backbone and especially with the inclusion of Require.js.

Many of you, myself included, will initially try to fight a concept like this and argue that you can accomplish it all using your typical jQuery event binding and DOM manipulation.  Having originally written this application in basically 1 gigantic core.js file I can tell you first hand that it just starts to get way to unwieldy. You just have functions on top of functions and things get out of hand way to fast. Debugging becomes a nightmare, and you have to think way to hard to ensure that you accounted for all of your events and every conceivable scenario.

I really hope you learned something reading through these articles! If you have any questions or comments please feel free to post. I hope I can learn a lot from you as well in the discussion! Thanks for reading!

A few requests for the full source code for this project have been made. Please feel free to download the full project files via this repo:
https://github.com/jkat98/imgettinit_netmvc

6 thoughts on “Building a web app using Backbone.js and require.js – Part 2

    • Thanks! No demo sorry. I am planning on doing a more robust build of this application and featuring it as a start-finish tutorial (some day). Most likely with a node.js backend.

      Like

  1. Hi Jason,

    I’ve recently been faced with the trials and tribulations of learning BB and RequireJS. Your tutorial is the closest thing out there that I’ve found for someone who wants to develop a MVC4 app using the client-side MV* with AMD and such. However, I was coding along with the article and the code is not all there. Any chance of you posting the source?

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s