Create a Frontend App in Backbone

We’ll be making a Superadmin Account Manager as P.O.C. frontend app.
The goal of this application is to manage our accounts and users. This post will cover the basic setup of the frontend logic, based on Node.js, Bower, Grunt, Backbone.js, Require.js and Bootstrap.
While this article's value is in the team approach (eg. Bower), the workflow is generally considered "best practice", even for solo projects.

The main purpose of the Superadmin is to visualise users and accounts we're going to create later on in the API.

Manage Packages

The days where tuts had to add their source files at the end of the post, are behind us. Thanks to composer, bower, brew and others, we can now keep the packages up to date like real grown up developers.

HomeBrew

We use HomeBrew for many of our tool installations, if you don't have installed your local copy yet, do it first (you'll thank me later)
If you have, it doesn't hurt to update to the latest version.

$ brew update
$ brew upgrade --all

Install tools

We've created an article for this purpose. To install Node, Grunt and Bower, head over there.

If you have the tools already installed but haven't used them in a while, make sure you have the latest versions.
$ sudo npm update -g npm grunt bower

Set up the project

For the project, we’ll be using a series of popular open source javascript packages: Require.js, jQuery, Backbone.js with Underscore and Backgrid, Mustache and Bootstrap for some visual candy.

Move to your favorite location to create the project folder. For this post, we’ll use ~/superadmin as reference.

We’ll be using a src folder for your working files, a staging folder folder for your local and development environments and finally a dist folder for your production release.
Go ahead and create them if you want.

$ cd ~/superadmin
$ mkdir src staging dist

Git prep

As any project, we're going to Git the Superadmin. We prefer GitHub, you even can find some articles on this blog eager assist you in the falling-in-love process.
For now however, we're just going to create a the ignore file and add our destination folders.

$ nano .gitignore
.DS_Store
staging  
dist  

Dependencies

We use Bower as dependency manager. There's a minimal config file called .bowerrc pointing bower to the right directory, and the bower.json dependencies file. The latter holds information about our own project, and the required external packages we'll use.

$ nano .bowerrc
{
  "directory" : "src/vendor"
}
$ nano bower.json
{
    "name": "superadmin",
    "version": "0.0.1",
    "authors": [
        "Koen Betsens <koen@cloudoki.com>"
    ],
    "dependencies": {
        "jquery": "*",
        "requirejs": "*",
        "underscore": "*",
        "backbone": "*",
        "mustache": "*",
        "bootstrap": "*",
        "backgrid": "*"
    }
}

We'll also be using Grunt as task runner, which has it's own dependencies. Let's create the node project file called package.json:

$ nano package.json
{
    "name": "Project-superadmin",
    "version": "0.0.1",
    "title" : "Project Superadmin",
    "description": "Superadmin application using Grunt, Backbone.js & Require.js",
    "homepage": "",
    "bugs": "",
    "keywords": [],
    "private": true,
    "contributors": [
        "Koen Betsens <koen@cloudoki.com>"
    ],
    "repository": {
        "type": "git",
        "url": "https://github.com/Project/superadmin"
    },
    "dependencies": {},
    "devDependencies": {
        "expect.js": "*",
        "grunt-notify": "*",
        "grunt-newer": "*",
        "load-grunt-tasks": "*",
        "grunt-contrib-jshint": "*",
        "grunt-contrib-clean": "*",
        "grunt-mustache": "*",
        "grunt-mustache-render": "*",
        "grunt-contrib-cssmin": "*",
        "grunt-contrib-copy": "*",
        "grunt-concurrent": "*",
        "grunt-contrib-watch": "*"
    },
    "scripts": {
        "initiate": "npm install; bower install; grunt staging; grunt watcher",
        "watch": "grunt watcher",
        "stage": "grunt staging",
        "release": "bower update; grunt release"
    }
}

By adding the scripts definition to the package.json file, we can call specific cli functions. This is however purely cosmetics, feel free to call grunt directly, eg. bower install.

Task Runner

Grunt allows us to define tasks like testing and compiling, amongst many - many - more. Our simple superadmin project gruntfile takes care of JS sanity testing, compression of css files, concatinating of template files and the templating process of the html files. We’ll have Grunt Watch monitoring changes and a Grunt Release for the deploy we send to the server.
We make a destinction between staging and release. The first will keep the compiled files readable for development purposes, the latter neatly packed for distribution.
Our Gruntfile.js is somewhat based on Paul Bakaus' tutorial.

$ nano Gruntfile.js

module.exports = function (grunt)  
{
    // load all grunt tasks
    require('load-grunt-tasks')(grunt);

    // Project configuration.
    grunt.initConfig(
    {
        pkg: grunt.file.readJSON('package.json'),

        dirs: {
            source: 'src',
            staging: 'staging',
            release: 'dist'
        },

        /* Testing */
        jshint: {
            options: {
                asi: true, eqnull: true, jquery: true
            },
            source: ['<%= dirs.source %>/js/**/*.js']
        },

        /* Cleaning */
        clean: {
            staging: ['<%= dirs.staging %>'],
            release: ['<%= dirs.release %>']
        },

        /* Build files */
        mustache_render: {
            staging: {
                files:
                [{
                    expand: true,
                    cwd: '<%= dirs.source %>/',
                    src: '*.html',
                    dest: '<%= dirs.staging %>/',
                    data: {
                        title: '<%= pkg.title %>',
                        description: '<%= pkg.description %>',
                        version: '<%= pkg.version %>',
                        files: {
                            stylesheets: grunt.file.expand({cwd: 'src'}, 'css/**/*.css').map(function(path){ return {src: '/' + path}; }),
                            scripts: grunt.file.expand({cwd: 'src'}, 'js/**/*.js').map(function(path){ return {src: '/' + path}; }),
                            templates: '/js/templates.js'
                        }
                    }
                }]
            },
            release: {
                files:
                [{
                    expand: true,
                    cwd: '<%= dirs.source %>/',
                    src: '*.html',
                    dest: '<%= dirs.release %>/',
                    data: {
                        title: '<%= pkg.title %>',
                        description: '<%= pkg.description %>',
                        version: '<%= pkg.version %>',
                        files: {
                            stylesheets: [{src: '/css/styles-<%= pkg.version %>.min.css'}],
                            scripts: grunt.file.expand({cwd: 'src'}, 'js/**/*.js').map(function(path){ return {src: '/' + path}; }),
                            templates: '/js/templates-<%= pkg.version %>.js'
                        }
                    }
                }]
            }
        },

        /* Compress files */
        cssmin: {
            combine: {
                files: {
                    '<%= dirs.release %>/css/styles-<%= pkg.version %>.min.css': [
                        '<%= dirs.source %>/css/**/*.css',
                        '!*.combine.css',
                        '!*.min.css'
                    ]
                }
            }
        },

        /* Copy and concatinate files */
        copy: {
            staging: {
                files: [
                    {expand: true, cwd: '<%= dirs.source %>', src: ['*.json','*.txt','*.ico','images/**','css/**','js/**','!js/**-default.js'], dest: '<%= dirs.staging %>/', filter: 'isFile'},
                    {expand: true, cwd: '<%= dirs.source %>/vendor', src: ['*/*.js','*/*.css','*/*.png','*/dist/**','*/lib/**',"!**/Gruntfile.js"], dest: '<%= dirs.staging %>/js/lib'}
                ]
            },
            release: {
                files: [
                    {expand: true, cwd: '<%= dirs.source %>', src: ['*.txt', '*.ico', 'images/**', 'css/**', 'js/**', '!js/**-default.js'], dest: '<%= dirs.release %>/', filter: 'isFile'},
                    {expand: true, cwd: '<%= dirs.source %>/vendor', src: ['*/*.js','*/*.css','*/*.png','*/dist/**','*/lib/**',"!**/Gruntfile.js"], dest: '<%= dirs.release %>/js/lib'}
                ]
            }
        },

        mustache: {
            staging : {
                src: '<%= dirs.source %>/templates/',
                dest: '<%= dirs.staging %>/js/templates.js',
                options: {
                    prefix: 'Templates = ',
                    postfix: ';'
                }
            },
            release : {
                src: '<%= dirs.source %>/templates/',
                dest: '<%= dirs.release %>/js/templates-<%= pkg.version %>.js',
                options: {
                    prefix: 'Templates = ',
                    postfix: ';'
                }
            }
        },

        /* Balance processes */
        concurrent: {
            staging: ['mustache_render:staging', 'copy:staging', 'mustache:staging'],
            release: ['mustache_render:release', 'cssmin', 'copy:release', 'mustache:release'],
            watch: ['newer:mustache_render:staging', 'newer:copy:staging', 'mustache:staging']
        },

        /* Watch the beast */
        watch: {
            options: {cwd: '<%= dirs.source %>'},
            files: ['*.html', '*.js','css/**/*.css','js/**/*.js','templates/**/*.mustache'],
            tasks: ['concurrent:watch']
        }
    });

    // Register tasks
    grunt.registerTask('staging', ['jshint:source', 'clean:staging', 'concurrent:staging']);
    grunt.registerTask('release', ['jshint:source', 'clean:release', 'concurrent:release']);
    grunt.registerTask('watcher', ['watch']);
};

./Gruntfile.js - If you take a close read, you'll notice this project uses plain old css instead of sass.

Config app

Since there’s quite a bit javascript floating around in this project, we’re going to use Require.js to only load what the project needs, when it’s needed. Actually, for complex Bootstrap model structures, there’s no ad hoc alternative.
We store the require file in ./src/js/main.js:

$ mkdir src/js
$ nano src/js/main.js

/**
 * Require dependencies
 */
require.config(  
{
    baseUrl: '/js/',
    paths: 
    {
        'jquery': 'lib/jquery/jquery',
        'underscore': 'lib/underscore/underscore',
        'backbone': 'lib/backbone/backbone',
        'bootstrap': 'lib/bootstrap/dist/js/bootstrap',
        'mustache': 'lib/mustache/mustache',
        'backgrid': 'lib/backgrid/lib/backgrid'
    },
    shim: 
    {
        'bootstrap': {
            deps: ['jquery'],
            exports: 'bootstrap'
        },
        'underscore': {
            exports: '_'
        },
        'backbone': {
            deps: ['underscore', 'jquery', 'mustache'],
            exports: 'backbone'
        },
        'backgrid': {
            deps: ['jquery','backbone','underscore'],
            exports: 'Backgrid'
        }
    }
});

/**
 * Set up the global project name   
 */
var Superadmin;

require(  
    ['backbone', 'bootstrap'],
    function(Backbone, bootstrap)
    {   
        // Start
    }
);

We’ll be working with app and authentication keys later on, so we need config files tailored for each environment. Be warned, this tends to be a bit tricky, where some developers accidently overwrite other’s local config files in a repo environment (I plead guilty).
As preventive measure, add this line to your .gitignore file before adding the config file:

src/js/config.js

Create the default config file:

$ nano src/js/config-default.js

define({  
    appid : "your-app-id",
    apiurl: "https://api.project.cc",
    authurl: "https://api.project.cc/auth"
});

Now copy the default as your local version:

$ cp src/js/config-default.js src/js/config.js

Index and run

Since most of the code resides in the Bower, Grunt and Require packages, the index file remains elegant. As you might notice, the html file is actually Mustache, so we have some dynamic power to differentiate between development ("staging") and production ("release") file inclusion.
Create the ./src/index.html file:

<!DOCTYPE html>  
<html lang="en">  
    <head>
        <meta charset="utf-8">
        <meta property="og:title" content="{{pkg.title}}" />
        <meta property="og:site_name" content="{{pkg.title}}" />
        <meta property="og:description" name="description" content="{{pkg.description}}" />
        <title>{{title}}</title>

        <link rel="icon" href="favicon.ico">
        {{#files.stylesheets}}
        <link href="{{{src}}}" rel="stylesheet">
        {{/files.stylesheets}}

        <!-- Vendor CSS -->
        <link href="/js/lib/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
        <link href="/js/lib/backgrid/lib/backgrid.css" rel="stylesheet">
    </head>

    <body>
        <nav class="navbar navbar-inverse navbar-static-top">
            <div class="container-fluid">
                <div class="navbar-header">
                    <a class="navbar-brand" href="/">{{title}}</a>
                </div>
            </div>
        </nav>

        <div class="container-fluid">
            <div class="row">
                <div id="sidebar" class="col-sm-3 col-md-2 sidebar"></div>
                <div id="page" class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">

                    <h1>Hello World</h1>
                    <blockquote>
                        <p>The more you find out about the world, the more opportunities there are to laugh at it.</p>
                        <footer>Bill Nye</footer>
                    </blockquote>
                </div>
            </div>
        </div>

        <!-- Templates -->
        <script src="{{{files.templates}}}" type="text/javascript"></script>

        <!-- Require -->
        <script data-main="/js/main" src="/js/lib/requirejs/require.js"></script>

    </body>
</html>  

You might remember we added the initiate script to our package.
We now can initiate the project from the base folder ~/superadmin:

$ npm run initiate

Use ctrl+c to quit the watch.

This will install all the dependencies for both the project and the task manager. The command will stay alive to watch, so it re-compiles your staging folder on changes, thanks to grunt-contrib-watch.

You might notice 2 module folders are created. Add them to .gitignore to keep your repo clean:

src/vendor  
node_modules  

Feel free to play around with npm staging, grunt watcher and other commands, to find out what suits you the most.
If you don’t have your local environment running on mac yet, head to our Nginx article.

Don't forget $ sudo nginx -s reload ...

Backbone Routes

Basics

We now have a nice, but almost completely empty bootstrap pageview. Let's start applying some backbone magic. Create the global project class first.

$ nano src/js/Superadmin.js

define(  
    ['backbone', 'Router', 'Session'],
    function (Backbone, Router, Session)
    {

Whoa, wait a minute. What you're looking at now is actually Require.js, defining the modules you'll need in the Superadmin class. Backbone is only logical, so is Router (read more). Session however is home grown, we use it later for page-like interactions.

        var Superadmin = {

            activate: function ()
            {
                // Initiate the router.
                this.router = new Router ();

                Backbone.history.start();
            }
        };

        return Superadmin;
    }
);

The actual Superadmin Class (object) contains functions we're going to use all-through the project. Activate starts the application, more will be added later.

To get the above working, we'll need to add the module dependency and activate function to src/js/main.js.

require(  
    ['backbone', 'bootstrap', 'Superadmin'],
    function(Backbone, bootstrap, sam)
    {    
      // Start
      sam.activate ();
    }
);

Let's not slack and create the Session Class (object) right away.

$ nano src/js/Session.js

define(  
    ['backbone'],
    function (Backbone)
    {
        var Session = {

            render: function ()
            {
                // Do some rendering
                $('#page').html (this.view.render ().el);
            },

            setView: function (view)
            {
                // Remove the old
                if (this.view) this.view.remove();

                Session.trigger('destroy:view');

                this.view = view;    

                this.render();
            }
        }

        // Add events
        _.extend(Session, Backbone.Events);

        return Session;
    }
);

The functions render and setView bind pageviews to our index file. Let's finish with the Router Class (object). The routes object contains the endpoints, the included functions bind the pageviews.

$ nano src/js/Router.js

define(  
    ['backbone', 'Session'],
    function (Backbone, Session)
    {
        var Router = Backbone.Router.extend (
        {
            routes :
            {
                '*path': 'hello'
            },

            hello : function ()
            {
                var View = Backbone.View.extend(
                {
                    render: function() {
                        this.$el.html("Hello App.");
                        return this;
                    }
                });

                Session.setView (new View ());
            }
        });

        return Router;
    }
);

If everything went fine, the Grunt Watch process kept track of all your changes, and you are now ready to test your first Backbone app. Use $ grunt staging alternatively. You may have to hard-refresh your cache.
What we've done so far, is setting up a scalable structure for a multipage webapp. The actual views are next.

You may remove the stale #page content from src/index.html now, if you feel like.

Views

One might have noticed the basic implementation of Backbone.View in the Router file. We are now going to expand both.
Let's start by creating our boilerplate and our first application view object, named dashboard, in a new js folder called... views. The actual html will be stored in the templates folder. Mustache takes care of the rest.

$ mkdir src/js/views src/templates
$ nano src/js/views/Pageview.js

define (  
    ['backbone', 'mustache'],
    function (Backbone, Mustache)
    {
        var Pageview = Backbone.View.extend({

            title: "Page",
            className: "container-fluid",
            span: 0,
            panels: [],
            panelviews: [],
            events: {
                'remove': 'destroy'
            },

            render: function ()
            {
                // Build Pageview
                this.$el.html (Mustache.render (Templates.pageview, {'title' : this.title}));

                // Panels parent
                this.$container = this.$el.find("#container").eq(0);

                // Append panels
                this.appendPanels();

                return this;
            },

            appendPanels: function() {

                for(var n in this.panels)
                {
                    var panel = this.panels[n].view(this.panels[n].data);

                    this.appendPanel(panel, this.panels[n].size);
                }
            },

            appendPanel: function(panel, span, padding) {

                if(!this.span || span === 0)
                {
                    this.$container.append(Templates.row);
                }

                this.span = (span + this.span < 12)? span + this.span : 0;

                if(panel)
                {    
                    this.$container.children().last().append( panel.render().el );

                    this.panelviews.push(panel);

                    panel.$el.addClass("col-md-" + span);
                }

                return this;
            },

            appendhtml: function(html)
            {
                this.$container.children().last().append(html);

                return this;
            },

            destroy: function ()
            {    
                $.each(this.panelviews, function(i, view){ view.remove() });
            }
        });

        return Pageview;
    }
);

The Pageview Class (object) contains a specific Backbone oriented modus operandi. It basicly takes away the burden of the grid behaviour, sugared with some memory-leak prevention.
As you might have noticed, some Mustache templates are loaded. Add them.

$ nano src/templates/pageview.mustache

<div class="page-header">  
    <h1>{{title}}</h1>
</div>

<div id="container" class="row"></div>  
$ nano src/templates/row.mustache

<div class="row"></div>  
$ nano src/templates/panel.mustache

<div class="panel panel-{{paneltype}}">  
    {{#title}}
    <div class="panel-heading">
        <h3 class="panel-title">{{title}}</h3>
    </div>
    {{/title}}

    <div class="panel-body">
        {{{body}}}
    </div>

    {{#footer}}
        <div class="panel-footer">{{{footer}}}</div>
    {{/footer}}
</div>  

Since the panel mustache sneaked in, we can just as well create that View now.

$ nano src/js/views/Panel.js

define (  
    ['backbone', 'mustache'],
    function (Backbone, Mustache)
    {
        var Panel = Backbone.View.extend({

            paneltype: 'default',
            events: {
                'remove' : 'destroy',
            },

            initialize: function (options)
            {
                if(options) $.extend(this, options);
            },

            render: function ()
            {    
                // Get template
                this.$el.html (Mustache.render (Templates.panel, this));

                return this;
            }
        });

        return Panel;
    }
);

The base view Pageview recurring logic is required in eg. Dashboard. We now can create that Class (object) as an extension.

$ nano src/js/views/Dashboard.js

define (  
    ['mustache', 'Views/Pageview', 'Views/Panel'],
    function (Mustache, Pageview, Panel)
    {
        var Dashboard = Pageview.extend(
        {
            title : "Dashboard",
            content: "<button id='users' class='btn btn-default'>Users</button><button id='accounts' class='btn btn-default'>Accounts</button>",

            render: function ()
            {
                // Build Pageview
                this.$el.html (Mustache.render (Templates.pageview, {'title' : this.title}));

                // Panels parent
                this.$container = this.$el.find("#container").eq(0);

                var buttons = new Panel ({title: this.title + ' panel', body: this.content});
                this.appendPanel (buttons, 4);

                return this;
            }

        });

        return Dashboard;
    }
);

Time to add the dashboard view to src/js/Router.js. You may remove hello and replace the code with the latter. Don't forget to add the dependency.

define(  
    ['backbone', 'Session', 'Views/Dashboard'],
    function (Backbone, Session, Dashboard)
    {
        var Router = Backbone.Router.extend (
        {
            routes :
            {
                '*path': 'dashboard'
            },

            dashboard : function ()
            {
                Session.setView (new Dashboard ());
            }
        });

        return Router;
    }
);

Go and take a look - your dashboard should have a Bootstrap panel with some nice buttons.
That's it for this article.


NEXT

Now go and read the API tutorial.
Once you have an API, Part 2 of this article is waiting just for you.


Notes

It is advised to keep your repo as clean as possible, by performing the actual grunt release when a deploy is finished on the target server. In order to make this work, you need a similar Nodejs & Grunt environment on your target machine, some .gitignore tweaks and a post-script running in your deploy software

.gitignore

.DS_Store
src/js/config.js  
src/vendor  
node_modules  
dist  
staging  

Dploy.io – Post-deployment commands

# npm update
# bower update
# grunt release

Go on and give the Superadmin a swirl.

comments powered by Disqus