Client-side program architecture

The basic idea is simple: the user searches for the name of someone on GitHub in the input box. When he enters a name, we fire a request to the API designed earlier in this chapter. When the response from the API returns, the program binds that response to a model and emits an event notifying that the model has been changed. The views listen for this event and refresh from the model in response.

Designing the model

Let's start by defining the client-side model. The model holds information regarding the repos of the user currently displayed. It gets filled in after the first search.

// public/javascripts/model.js

define([], function(){
   return {
    ghubUser: "", // last name that was searched for
    exists: true, // does that person exist on github?
    repos: [] // list of repos
  } ;
});

To see a populated value of the model, head to the complete application example on app.scala4datascience.com, open a JavaScript console in your browser, search for a user (for example, odersky) in the application and type the following in the console:

> require(["model"], function(model) { console.log(model) ; }) 
{ghubUser: "odersky", exists: true, repos: Array}

> require(["model"], function(model) { 
  console.log(model.repos[0]); 
})
{name: "dotty", language: "Scala", is_fork: true, size: 14653}

These import the "model" module, bind it to the variable model, and then print information to the console.

The event bus

We need a mechanism for informing the views when the model is updated, since the views need to refresh from the new model. This is commonly handled through events in web applications. JQuery lets us bind callbacks to specific events. The callback is executed when that event occurs.

For instance, to bind a callback to the event "custom-event", enter the following in a JavaScript console:

> $(window).on("custom-event", function() { 
  console.log("custom event received") ; 
});

We can fire the event using:

> $(window).trigger("custom-event"); 
custom event received

Events in JQuery require an event bus, a DOM element on which the event is registered. In this case, we used the window DOM element as our event bus, but any JQuery element would have served. Centralizing event definitions to a single module is helpful. We will, therefore, create an events module containing two functions: trigger, which triggers an event (specified by a string) and on, which binds a callback to a specific event:

// public/javascripts/events.js

define(["jquery"], function($) {

  var bus = $(window) ; // widget to use as an event bus

  function trigger(eventType) {
    $(bus).trigger(eventType) ;
  }

  function on(eventType, f) {
    $(bus).on(eventType, f) ;
  }

  return {
    "trigger": trigger,
    "on": on
  } ;
});

We can now emit and receive events using the events module. You can test this out in a JavaScript console on the live version of the application (at app.scala4datascience.com). Let's start by registering a listener:

> require(["events"], function(events) {
  // register event listener
  events.on("hello_event", function() {
    console.log("Received event") ;
  }) ;
}); 

If we now trigger the event "hello_event", the listener prints "Received event":

> require(["events"], function(events) {
  // trigger the event
  events.trigger("hello_event") ;
}) ;

Using events allows us to decouple the controller from the views. The controller does not need to know anything about the views, and vice-versa. The controller just needs to emit a "model_updated" event when the model is updated, and the views need to refresh from the model when they receive that event.

AJAX calls through JQuery

We can now write the controller for our application. When the user enters a name in the text input, we query the API, update the model and trigger a model_updated event.

We use JQuery's $.getJSON function to query our API. This function takes a URL as its first argument, and a callback as its second argument. The API call is asynchronous: $.getJSON returns immediately after execution. All request processing must, therefore, be done in the callback. The callback is called if the request is successful, but we can define additional handlers that are always called, or called on failure. Let's try this out in the browser console (either your own, if you are running the API developed in the previous chapter, or on app.scala4datascience.com). Recall that the API is listening to the end-point /api/repos/:user:

> $.getJSON("/api/repos/odersky", function(data) { 
  console.log("API response:");
  console.log(data);
  console.log(data[0]); 
}) ;
{readyState: 1, getResponseHeader: function, ...}

API response:
[Object, Object, Object, Object, Object, ...]
{name: "dotty", language: "Scala", is_fork: true, size: 14653}

getJSON returns immediately. A few tenths of a second later, the API responds, at which point the response gets fed through the callback.

The callback only gets executed on success. It takes, as its argument, the JSON object returned by the API. To bind a callback that is executed when the API request fails, call the .fail method on the return value of getJSON:

> $.getJSON("/api/repos/junk123456", function(data) { 
  console.log("called on success"); 
}).fail(function() { 
  console.log("called on failure") ; 
}) ;
{readyState: 1, getResponseHeader: function, ...}

called on failure

We can also use the .always method on the return value of getJSON to specify a callback that is executed, whether the API query was successful or not.

Now that we know how to use $.getJSON to query our API, we can write the controller. The controller listens for changes to the #user-selection input field. When a change occurs, it fires an AJAX request to the API for information on that user. It binds a callback which updates the model when the API replies with a list of repositories. We will define a controller module that exports a single function, initialize, that creates the event listeners:

// public/javascripts/controller.js
define(["jquery", "events", "model"], function($, events, model) {

  function initialize() {
    $("#user-selection").change(function() {

      var user = $("#user-selection").val() ;
      console.log("Fetching information for " + user) ;

      // Change cursor to a 'wait' symbol 
      // while we wait for the API to respond
      $("*").css({"cursor": "wait"}) ; 

      $.getJSON("/api/repos/" + user, function(data) {
        // Executed on success
        model.exists = true ;
        model.repos = data ;
      }).fail(function() {
        // Executed on failure
        model.exists = false ;
        model.repos = [] ;
      }).always(function() {
        // Always executed
        model.ghubUser = user ;

        // Restore cursor
        $("*").css({"cursor": "initial"}) ;

        // Tell the rest of the application 
        // that the model has been updated.
        events.trigger("model_updated") ;
      });
    }) ;
  } ;

  return { "initialize": initialize };

});

Our controller module just exposes the initialize method. Once the initialization is performed, the controller interacts with the rest of the application through event listeners. We will call the controller's initialize method in main.js. Currently, the last lines of that file are just an empty require block. Let's import our controller and initialize it:

// public/javascripts/main.js

require(["controller"], function(controller) {
  controller.initialize();
});

To test that this works, we can bind a dummy listener to the "model_updated" event. For instance, we could log the current model to the browser JavaScript console with the following snippet (which you can write directly in the JavaScript console):

> require(["events", "model"], 
function(events, model) {
  events.on("model_updated", function () { 
    console.log("model_updated event received"); 
    console.log(model); 
  });
}); 

If you then search for a user, the model will be printed to the console. We now have the controller in place. The last step is writing the views.

Response views

If the request fails, we just display Not found in the response div. This part is the easiest to code up, so let's do that first. We define an initialize method that generates the view. The view then listens for the "model_updated" event, which is fired by the controller after it updates the model. Once the initialization is complete, the only way to interact with the response view is through "model_updated" events:

// public/javascripts/responseView.js

define(["jquery", "model", "events"],
function($, model, events) {

  var failedResponseHtml = 
    "<div class='col-md-12'>Not found</div>" ;

  function initialize() {
    events.on("model_updated", function() {
      if (model.exists) {
        // success – we will fill this in later.
        console.log("model exists")
      }
      else {
        // failure – the user entered
        // is not a valid GitHub login 
        $("#response").html(failedResponseHtml) ;
      }
    }) ;
  }

  return { "initialize": initialize } ;

});

To bootstrap the view, we must call the initialize function from main.js. Just add a dependency on responseView in the require block, and call responseView.initialize(). With these modifications, the final require block in main.js is:

// public/javascripts/main.js

require(["controller", "responseView"],
function(controller, responseView) {
  controller.initialize();
  responseView.initialize() ;
}) ;

You can check that this all works by entering junk in the user input to deliberately cause the API request to fail.

When the user enters a valid GitHub login name and the API returns a list of repos, we must display those on the screen. We display a table and a pie chart that aggregates the repository sizes by language. We will define the pie chart and the table in two separate modules, called repoGraph.js and repoTable.js. Let's assume those exist for now and that they expose a build method that accepts a model and the name of a div in which to appear.

Let's update the code for responseView to accommodate the user entering a valid GitHub user name:

// public/javascripts/responseView.js

define(["jquery", "model", "events", "repoTable", "repoGraph"],
function($, model, events, repoTable, repoGraph) {

  // HTHML to inject when the model represents a valid user 
  var successfulResponseHtml = 
    "<div class='col-md-6' id='response-table'></div>" +
    "<div class='col-md-6' id='response-graph'></div>" ;

  // HTML to inject when the model is for a non-existent user
  var failedResponseHtml = 
    "<div class='col-md-12'>Not found</div>" ;

  function initialize() {
    events.on("model_updated", function() {
      if (model.exists) {
        $("#response").html(successfulResponseHtml) ;
        repoTable.build(model, "#response-table") ;
        repoGraph.build(model, "#response-graph") ;
      }
      else {
        $("#response").html(failedResponseHtml) ;
      }
    }) ;
  }

  return { "initialize": initialize } ;

});

Let's walk through what happens in the event of a successful API call. We inject the following bit of HTML in the #response div:

var successfulResponseHtml = 
  "<div class='col-md-6' id='response-table'></div>" +
  "<div class='col-md-6' id='response-graph'></div>" ;

This adds two HTML divs, one for the table of repositories, and the other for the graph. We use Bootstrap classes to split the response div vertically.

Let's now turn our attention to the table view, which needs to expose a single build method, as described in the previous section. We will just display the repositories in an HTML table. We will use Underscore templates to build the table dynamically. Underscore templates work much like string interpolation in Scala: we define a template with placeholders. Let's try this in a browser console:

> require(["underscore"], function(_) {
  var myTemplate = _.template(
    "Hello, <%= title %> <%= name %>!"
  ) ;
});

This creates a myTemplate function which accepts an object with attributes title and name:

> require(["underscore"], function(_) {
  var myTemplate = _.template( ... ); 
  var person = { title: "Dr.", name: "Odersky" } ;
  console.log(myTemplate(person)) ;
});

Underscore templates thus provide a convenient mechanism for formatting an object as a string. We will create a template for each row in our table, and pass the model for each repository to the template:

// public/javascripts/repoTable.js

define(["underscore", "jquery"], function(_, $) {

  // Underscore template for each row
  var rowTemplate = _.template("<tr>" +
    "<td><%= name %></td>" +
    "<td><%= language %></td>" +
    "<td><%= size %></td>" +
    "</tr>") ;

  // template for the table
  var repoTable = _.template(
    "<table id='repo-table' class='table'>" +
      "<thead>" +
        "<tr>" +
          "<th>Name</th><th>Language</th><th>Size</th>" +
        "</tr>" +
      "</thead>" +
      "<tbody>" +
        "<%= tbody %>" +
      "</tbody>" +
    "</table>") ;

  // Builds a table for a model
  function build(model, divName) {
    var tbody = "" ;
    _.each(model.repos, function(repo) {
      tbody += rowTemplate(repo) ;
    }) ;
    var table = repoTable({tbody: tbody}) ;
    $(divName).html(table) ;
  }

  return { "build": build } ;
}) ;
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.246.245