Monday, 29 April 2013

Simulation of Google Line Chart real time updates with Backbone.js and Require.js


This Backbone.js an Require.js AMD app is designed to load modular code asynchronously in the browser.

On the report section a simple line chart is created using google visualization API. Real  time updates of the chart are simulated by using a timer to add data incrementally to the chart.


It contains 2 separate views (about and reports sections) which are defined by a router and can be access directly via URL. It retrieves a collection, from a JSON API external to the application. It relies on google line chart visualization library to render the collection. Real – time updates are simulated by inserting extra rows  of cells that contain randomly generated data.  Nice transition animation is also included.
As usual we start by preparing our index.html page in the root directory:
index.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Simple Line Chart</title>
        <link href="css/bootstrap.css" rel="stylesheet">
        <script data-main="js/main" src="js/libs/require.js"></script>
    </head>
    <body>
        <div class="navbar">
            <div class="navbar-inner">
                <div class="container">
                    <a class="brand" href="#">
                        Simple Line Chart
                    </a>
                    <ul class="nav">
                        <li><a href="#about">About</a></li>
                        <li><a href="#reports">Reports</a></li>
                    </ul>
                </div>
            </div>
        </div>
        <div id="content"></div>

        <!-- Templates -->
        <script type="text/template" id="aboutApp">
            <table class="table table-bordered table-striped">
  <tr>
    <td>Create a small app using Require.js  and Backbone.js and other libraries of your choice with a reports section module.</td>
    <td>This Backbone.js an Require.js AMD app is designed to load modular code asynchronously in the browser.</td>
  </tr>
  <tr>
    <td>On the report section create a simple line chart using a library of your choice. Simulate real – time updates of the chart by using a timer to add data incrementally to the chart. Optionally include transition animation.</td>
    <td>It contains 2 separate views (about and reports sections) which are defined by a router and can be access directly via URL. It retrieves a collection, from a JSON API external to the application. It relies on google line chart visualization library to render the collection. Real – time updates are simulated by inserting extra rows  of cells that contain randomly generated data.  Nice transition animation is included.</td>
  </tr>
  <tr>
    <td>Include a nice layout using CSS.  Create clear separation between model , view , and controllers using OO inheritance , encapsulation, abstraction, and polymorphism.</td>
    <td>It uses Twitter Bootstrap framework which was made to not only look and behave great in the latest desktop browsers (as well as IE7!), but in tablet and smartphone browsers via responsive CSS as well. Backbone.js library provides structure to web applications by using models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface. In this test we configure Require.js to create a shortcut alias to commonly used scripts such as jQuery, Underscore and Backbone.</td>
  </tr>
  <tr>
    <td>The application should be able to scale with development. For example, the architecture should work to allow new modules and features added in the future.</td>
    <td>Applications are not created equal and the reality is that the answer probably consists of a collection of patterns that can be used as necessary given the situation. At the end of the day the goal should be to create a code-base that is easy to understand, implement and maintain. ;</td>
  </tr>
</table>
        </script>
        <script type="text/template" id="lineChart">
  <p>Simple line chart - in draw visualization:</p>
        <div id="lc" style="width:600px; height:300px;"></div>
        </script>
    </body>
</html>
The HTML file tells Require.js to execute the main.js file in the script directory.
main.js
// Require.js allows us to configure shortcut alias
require.config({
 paths: {
  jquery: 'libs/jquery',
  underscore: 'libs/underscore',
  backbone: 'libs/backbone'
 },
 shim: {
  underscore: {
   exports: "_"
  },
  backbone: {
   deps: ['underscore', 'jquery'],
   exports: 'Backbone'
  }
 }
});
require([
// Load our app module and pass it to our definition function
'app', ], function(App) {
 // The "app" dependency is passed in as "App"
 App.initialize();
});
We build our application main module in app.js:
app.js
define(['jquery', 'underscore', 'backbone', 'router' // Request router.js
], function($, _, Backbone, Router) {
 var initialize = function() {
  // Pass in our Router module and call it's initialize function
  Router.initialize();
 }
 return {
  initialize: initialize
 };
});
After initiliasing our main module we can set up the Backbone Router will load the correct dependencies depending on the current URL.
router.js
define(['jquery', 'underscore', 'backbone', 'views/titleListView', 'views/resultsListView'], function($, _, Backbone, AboutListView, ResultsListView) {
 //Define Namespace (RL shorthand for report list)
 var RL = RL || {};
 /* App Router */
 var AppRouter = Backbone.Router.extend({
  routes: {
   "about": "listAbout",
   "reports": "listReports",
   "": "listAbout"
  },
  initialize: function() {
   this.about = new RL.AboutCollection();
   this.lineChart = new RL.ChartCollection();
   this.lineChart.fetch();
   this.aboutView = new AboutListView({
    model: this.about
   });
   this.resultsView = new ResultsListView({
    model: this.lineChart
   });
  },
  listAbout: function() {
   window.clearInterval(this.interval)
   $('#content').html(this.aboutView.render().el);
  },
  listReports: function() {
   this.interval = window.setInterval(_.bind(this.resultsView.onTimerTick, this.resultsView), 1000);
   $('#content').html(this.resultsView.render().el);
  }
 });
 //Models
 //A About
 RL.About = Backbone.Model.extend({});
 // Collections 
 RL.AboutCollection = Backbone.Collection.extend({
  model: RL.About
 });
 // Collections 
 RL.ChartCollection = Backbone.Collection.extend({
  model: RL.About,
  url: "data/lineChart.json"
 });
 var initialize = function() {
  RL.app = new AppRouter();
  Backbone.history.start();
 }
 return {
  initialize: initialize
 };
});
Using our modular views we interact with the DOM and load in JavaScript templates.
resultListView.js
define(['jquery', 'underscore', 'backbone', 'https://www.google.com/jsapi'], function($, _, Backbone) {
 var ResultsListView = Backbone.View.extend({
  template: _.template($('#lineChart').html()),
  onTimerTick: function() {
   //removes the first row
   this.data.removeRow(0);
   //static var x is a tick counter
   this.x || (this.x = this.data.getNumberOfRows() + 1);
   this.x++;
   // Generating a random a, b pair and inserting it so rows are sorted.
   var a = Math.floor(Math.random() * 100);
   var b = Math.floor(Math.random() * 100);
   //adds extra row
   this.data.insertRows(this.data.getNumberOfRows(), [
    [this.x.toString(), a, b]
   ]);
   this.chart.draw(this.data, this.options);
  },
  render: function() {
   google.load('visualization', '1', {
    'callback': _.bind(this.drawVisualization, this),
    'packages': ['corechart']
   });
   return this;
  },
  drawVisualization: function() {
   this.data = new google.visualization.DataTable(this.model.models[0].attributes);
   //In draw visualization
   this.options = {
    animation: {
     duration: 800,
     easing: 'out',
    },
    vAxis: {
     minValue: 0,
     maxValue: 100
    }
   }
   $(this.el).html(this.template());
   this.chart = new google.visualization.LineChart(this.$('#lc').get(0));
   this.chart.draw(this.data, this.options);
  }
 });
 // Our module now returns our view
 return ResultsListView;
});

titleListView.js
define(['jquery', 'underscore', 'backbone'], function($, _, Backbone) {
 var TitleListView = Backbone.View.extend({
  initialize: function() {
   this.model.bind("reset", this.render, this);
  },
  template: _.template($('#aboutApp').html()),
  render: function(e) {
   $(this.el).html(this.template());
   return this;
  }
 });
 // Our module now returns our view
 return TitleListView;
});

It uses Twitter Bootstrap framework which was made to not only look and behave great in the latest desktop browsers (as well as IE7!), but in tablet and smartphone browsers via responsive CSS as well. Backbone.js library provides structure to web applications by using models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface. In this test we configure Require.js to create a shortcut alias to commonly used scripts such as jQuery, Underscore and Backbone.

The application is scalable - e.g.: the architecture should work to allow new modules and features added in the future. In order to create a code-base that is easy to understand, implement and maintain  a clear separation between model , view, and controllers using OO inheritance, encapsulation, abstraction, and polymorphism is implemented.
  
You can fork this AMD app here: 

Saturday, 6 April 2013

Table Football score list with Backbone.js and localStorage


Once upon a time there was a small company filled with talented and hard working individuals who played table football all the time. They were so competitive they thought it'd be a great idea to record the scores on a web app. However, they never had a chance to finish completing the app. They loved Backbone.js and Twitter Bootstrap, so they thought it was a natural choice to use it. All there was left to do was to find someone to get the job done...

The app needed some way of adding and editing table football results and players with persisting these beyond the browser session.


They also wanted the winners name to be highlighted  in yellow so they could easily see who's won a match.


Another requirement was that the results had to be saved in browsers localStorage and displayed when players return to the page.

This apps had to use Underscore’s template function. The template that had to be passed into it was  required to have a bi-directional binding (both model and DOM updates) :

HTML Template:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Table Football Scores</title>
<link href="css/bootstrap.css" rel="stylesheet">
</head>
<body>
<div class="navbar">
  <div class="navbar-inner">
    <div class="container"> <a class="brand" href="#"> Table Football Scores </a>
      <ul class="nav">
        <li><a href="#players">Player List</a></li>
        <li><a href="#results">Results List</a></li>
        <li><a href="#addplayer">Add Player</a></li>
        <li><a href="#addresult">Add Result</a></li>
        <li><a href="#editplayer">Edit Players</a></li>
        <li><a href="#editresult">Edit Results</a></li>
      </ul>
    </div>
  </div>
</div>
<div id="content"></div>
<!-- Templates --> 
<script type="text/template" id="tpl-player-list-item">
            <td><a href='#playerDetail/<%= id %>'><%= firstName %> <%= lastName %></a></td>
        </script> 
<script type="text/template" id="tpl-result-list-item">
            <td><%= player1.name %></td>
            <td><%= player1.score %></td>
            <td><%= player2.name %></td>
            <td><%= player2.score %></td>
        </script> 
<script type="text/template" id="nameInput">
  <form action="#" id="playerName">
     <label>First Name:</label><input class="firstName" value=""/>
  <label>Last Name:</label> <input class="lastName" value=""/>
     <button id="addPlayerName">Save</button>
  </form>
  </script> 
<script type="text/template" id="resultInput">
  <form action="#">
  <div id="filter1"><label>Player 1:</label></div>
  <label>Result Player 1:</label><input class="result1" value=""/>
  <div id="filter2"><label>Player 2:</label></div>
  <label>Result Player 2:</label><input class="result2" value="" />
  <button id="addResults">Save</button>
  </form>
  </script> 
<script type="text/template" id="playerEdit">
  <form action="#">
  <div id="playerSelect"><label>Select a Player:</label></div>
  <label>First Name:</label><input class="firstName" value=""/>
  <label>Last Name:</label> <input class="lastName" value=""/>
  <button id="editPlayer">Save</button>
  </form>
  </script> 
<script type="text/template" id="resultEdit">
  <form action="#">
  <div id="gameSelect"><label>Select a Game:</label></div>
  <div id="filter3"><label>Player 1:</label></div>
  <label>Result Player 1:</label><input class="result1" value=""/>
  <div id="filter4"><label>Player 2:</label></div>
  <label>Result Player 2:</label><input class="result2" value="" />
  <button id="editResult">Save</button>
  </form>
  </script> 
<!-- JS Scripts --> 
<script type="text/javascript" src="libs/underscore.js"></script> 
<script type="text/javascript" src="libs/jquery-1.6.2.js"></script> 
<script type="text/javascript" src="libs/backbone.js"></script> 
<script type="text/javascript" src="js/backbone.localStorage.js"></script> 
<script type="text/javascript" src="js/app.js"></script>
</body>
</html>
Applying <%= and %> tags had to make the template function replace those areas with the object properties defined in separate JSON files (one for results and one for the players). Model binding links had to be established  between the data in the models and mark-up shown by the views.

JavaScript View:
var TFS = TFS || {};
var AppRouter = Backbone.Router.extend({
    routes: {
        "results": "listResults",
        "players": "listPlayers",
        "addresult": "addResult",
        "addplayer": "addPlayer",
        "editresult": "editResult",
        "editplayer": "editPlayer",
        "": "listPlayers"
    },
    initialize: function () {
        this.results = new TFS.ResultCollecton();
        this.players = new TFS.PlayerCollection();
        this.resultsView = new TFS.ResultsListView({
            model: this.results
        });
        this.playersView = new TFS.PlayerListView({
            model: this.players
        });
        this.players.fetch();
        var rs = "ResultsStore";
        var rc = this.results;
        if (localStorage.getItem(rs)) {
            rc.localStorage = new Backbone.LocalStorage(rs);
            rc.fetch();
        } else {
            rc.fetch().done(function () {
                rc.localStorage = new Backbone.LocalStorage(rs);
                _.each(rc.models, function (r) {
                    r.save()
                });
            });
        }
    },
    listPlayers: function () {
        $('#content').html(this.playersView.render().el);
    },
    listResults: function () {
        $('#content').html(this.resultsView.render().el);
    },
    addPlayer: function () {
        var addPlayerView = new TFS.AddPlayerView();
        $('#content').html(addPlayerView.render().el);
    },
    addResult: function () {
        var addResultsView = new TFS.AddResultsView({
            model: this.players
        });
        $('#content').html(addResultsView.render().el);
    },
    editPlayer: function () {
        var editPlayerView = new TFS.EditPlayerView({
            model: this.players
        });
        $('#content').html(editPlayerView.render().el);
    },
    editResult: function () {
        var editResultsView = new TFS.EditResultsView({
            model: this.results
        });
        $('#content').html(editResultsView.render().el);
    },
});
TFS.Player = Backbone.Model.extend({});
TFS.Result = Backbone.Model.extend({});
TFS.PlayerCollection = Backbone.Collection.extend({
    model: TFS.Player,
    url: "data/players.json"
});
TFS.ResultCollecton = Backbone.Collection.extend({
    model: TFS.Result,
    url: "data/results.json"
});
TFS.AddResultsView = Backbone.View.extend({
    template: _.template($('#resultInput').html()),
    initialize: function () {
        this.model.bind("reset", this.render, this);
    },
    events: {
        "click #addResults": "saveResults",
    },
    saveResults: function (e) {
        e.preventDefault();
        var saveObj = TFS.validateResults(e.target);
        if (saveObj.msg) {
            alert(saveObj.msg);
        } else {
            e.target.parentNode.reset();
            TFS.app.results.create(saveObj.resultEntry);
            alert("New result has been added.")
        }
    },
    render: function () {
        $(this.el).html(this.template());
        $(this.el).find("#filter1").append(TFS.createPlayerSelect());
        $(this.el).find("#filter2").append(TFS.createPlayerSelect());
        return this;
    }
})
TFS.validateResults = function (t) {
    var o = {
        msg: "",
        resultEntry: {}
    };
    var formData = [];
    var selData = [];
    var inputs = $(t).siblings("input");
    var selections = $(t).siblings("div").find("select");

    function isNormalInteger(str) {
        var n = ~~Number(str);
        return String(n) === str && n >= 0;
    }
    var l = inputs.length;
    while (l) {
        var el = inputs[l - 1];
        if (isNormalInteger($(el).val())) {
            formData.push($(el).val());
        } else {
            o.msg = "Note: Enter positive numeric value in the Result fields.";
            return o;
        }--l;
    }
    var l = selections.length;
    while (l) {
        var el = selections[l - 1];
        if ($(el).val() !== "" && !~selData.indexOf($(el).val())) {
            selData.push($(el).val());
        } else {
            o.msg = "Please select different players' names from the lists";
            return o;
        }--l;
    }
    o.resultEntry = {
        player1: {
            name: selData[0],
            score: formData[0]
        },
        player2: {
            name: selData[1],
            score: formData[1]
        }
    }
    return o;
}
TFS.createPlayerSelect = function () {
    var select = $("<select/>", {
        html: "<option value=''>Select a Player</option>"
    });
    _.each(TFS.app.players.models, function (item) {
        var option = $("<option/>", {
            value: item.attributes.firstName,
            text: item.attributes.firstName
        }).appendTo(select);
    });
    return select;
}
TFS.AddPlayerView = Backbone.View.extend({
    template: _.template($('#nameInput').html()),
    events: {
        "click #addPlayerName": "saveName",
    },
    saveName: function (e) {
        e.preventDefault();
        var formData = {
            id: TFS.app.players.length + 1,
        };
        var inputs = $(e.target).siblings("input");
        var l = inputs.length;
        while (l) {
            var el = inputs[l - 1];
            if ($(el).val() !== "") {
                formData[el.className] = $(el).val();
            } else {
                alert("Please enter first and last names");
                return false;
            }--l;
        }
        TFS.app.players.add(formData);
        e.target.parentNode.reset();
        alert("New player has been added to the list");
    },
    render: function () {
        $(this.el).html(this.template());
        return this;
    }
});
TFS.EditPlayerView = Backbone.View.extend({
    template: _.template($('#playerEdit').html()),
    initialize: function () {
        this.model.bind("reset", this.render, this);
    },
    events: {
        "click #editPlayer": "saveResults",
        "change #playerSelect select": "setFilter"
    },
    setFilter: function (e) {
        var selected = $(e.currentTarget).find('option:selected');
        var inputs = $(this.el).find("#playerSelect ~ input");
        $(inputs[0]).val($(selected).attr("data-firstName"));
        $(inputs[1]).val($(selected).attr("data-lastName"));
    },
    createGameSelect: function () {
        var select = $("<select/>", {
            html: "<option value=''>Please select</option>"
        });
        _.each(this.model.models, function (item) {
            var option = $("<option/>", {
                value: item.attributes.id,
                text: item.attributes.id,
                "data-firstName": item.attributes.firstName,
                "data-lastName": item.attributes.lastName
            }).appendTo(select);
        });
        return select;
    },
    saveResults: function (e) {
        e.preventDefault();
        var selection = $(e.target).siblings("div").find("select").val();
        if (!selection) {
            alert("Note: Select a player");
            return false;
        }
        var inputs = $(e.target).siblings("input");
        var l = inputs.length;
        while (l) {
            if ($(inputs[l - 1]).val() == "") {
                alert("Note: First or last name is blank");
                return false;
            }--l;
        }
        this.model.models[selection - 1].set({
            firstName: $(inputs[0]).val(),
            lastName: $(inputs[1]).val()
        })
        e.target.parentNode.reset();
        alert("Player details have been edited.")
    },
    render: function () {
        $(this.el).html(this.template());
        $(this.el).find("#playerSelect").append(this.createGameSelect());
        return this;
    }
});
TFS.EditResultsView = Backbone.View.extend({
    template: _.template($('#resultEdit').html()),
    events: {
        "click #editResult": "saveResults",
    },
    initialize: function () {
        this.model.bind("reset", this.render, this);
        TFS.app.players.bind("reset", this.render, this);
    },
    createSelect: function () {
        var select = $("<select/>", {
            html: "<option value=''>Select a game</option>"
        });
        var i = 0;
        _.each(this.model.models, function (item) {
            i++;
            var option = $("<option/>", {
                value: i,
                text: i,
                "data-firstPlayer": item.attributes.player1,
                "data-secondPlayer": item.attributes.player2
            }).appendTo(select);
        });
        return select;
    },
    saveResults: function (e) {
        e.preventDefault();
        var selection = $(e.target).siblings("div").find("select").val();
        if (!selection) {
            alert("Note: Select a Game");
            return false;
        }
        var saveObj = TFS.validateResults(e.target);
        if (saveObj.msg) {
            alert(saveObj.msg);
            return false;
        }
        selection--;
        var m = this.model.models[selection];
        m.id = this.model.localStorage.records[selection]
        m.set({
            player1: saveObj.resultEntry.player1,
            player2: saveObj.resultEntry.player2
        })
        this.model.localStorage.update(m);
        alert("Result entry has been modified");
        e.target.parentNode.reset();
    },
    render: function () {
        $(this.el).html(this.template());
        $(this.el).find("#gameSelect").append(this.createSelect());
        $(this.el).find("#filter3").append(TFS.createPlayerSelect());
        $(this.el).find("#filter4").append(TFS.createPlayerSelect());
        return this;
    }
});
TFS.PlayerListView = Backbone.View.extend({
    tagName: "table",
    className: "table table-bordered table-striped",
    initialize: function () {
        this.model.bind("reset", this.render, this);
    },
    render: function (eventName) {
        $(this.el).empty();
        _.each(this.model.models, function (player) {
            $(this.el).append(new TFS.SinglePlayerView({
                model: player
            }).render().el);
        }, this);
        return this;
    }
});
TFS.SinglePlayerView = Backbone.View.extend({
    tagName: "tr",
    template: _.template($('#tpl-player-list-item').html()),
    render: function (eventName) {
        $(this.el).html(this.template(this.model.toJSON()));
        return this;
    }
});
TFS.SingleResultView = Backbone.View.extend({
    tagName: "tr",
    template: _.template($('#tpl-result-list-item').html()),
    render: function (eventName) {
        var re = this.model.toJSON();
        $(this.el).html(this.template(re));
        var sc1 = re.player1.score,
            sc2 = re.player2.score;
        if (sc1 !== sc2) {
            $(this.el).find("td:contains(" + Math.max(sc1, sc2) + ")").prev().css("background-color", "yellow");
        }
        return this;
    }
});
TFS.ResultsListView = Backbone.View.extend({
    tagName: "table",
    className: "table table-bordered table-striped",
    initialize: function () {
        this.model.bind('change', this.render, this);
    },
    render: function (eventName) {
        $(this.el).empty();
        _.each(this.model.models, function (wine) {
            $(this.el).append(new TFS.SingleResultView({
                model: wine
            }).render().el);
        }, this);
        return this;
    }
});
TFS.app = new AppRouter();
Backbone.history.start();

They expected the task to take no longer than an hour to get working to a reasonable standard. They were not too judgemental on UI, it's the code they cared about.

So if you were looking for a table football application with high-score table here it is: 
https://github.com/dondido/table-football-score