intertwingly

It’s just data

Software in 2014


Tim Bray: We’re at an inflection point in the practice of constructing software. Our tools are good, our server developers are happy, but when it comes to building client-side software, we really don’t know where we’re going or how to get there.

While I agree with much of this post, I really don’t think the conclusion is as bad as Tim portrays things. I agree that there are good server side frameworks, and doing things like MVC is the way to go.

I just happen to believe that this is true on the client too – including MVC. Not perfect, perhaps, but more than workable. And full disclosure, I’m firmly on the HTML5-rocks side of the fence.

For starters, while JavaScript is perfectly satisfactory language for many, it does seem to have accumulated some weird quirks. None of that, however, makes JavaScript any less desirable as a compilation target.

While I agree that jQuery reduces the pain of accessing the DOM, there is a future in sight where user authored jQuery will largely be a thing of the past. While I wouldn’t have believed it, I’ve seen it for myself, and I’ll describe it more below.

But first, let’s talk about MVC.

Client Side MVC

On the server, the model is data. On the client, the model can be data too – either from Web Storage or from the server. But it can also be accelerometers, cameras, and contacts. If you need some the these, take a peek at Apache Cordova.

For views, there are HTML fragments, mustache and mustache inspired syntaxes for templating, and yes, CSS. While Sass and its ilk reduce the pain here; a good alternative is to pick up a library like Bootstrap. You might need to tweak it a little, but there is a lot there and the peole who wrote it probably thought about a lot of problems that you may not have thought deeply about. For starters, their markup is responsive, which means that it automatically adjusts based on device characteristics.

Finally, we come to the controller. And by association, routing. There are good frameworks for this, too, on the client. Angular.js and Ember are two exemplars.

For the remainder of this, I’m going to focus on Angular.js. As near as I can tell, a very similar story could be told about Ember. I just happen to be writing an application using Angular.js, and can point to it.

Let me start by describing the application. I’m writing it partly to scratch an itch, and partly to learn a new framework. Before I started, I had never used Angular.js before. I will say that the learning curve for Angular.js (at least for me) is an “S” curve… easy to get started, then it gets harder to advance, then it gets easier again. I seem to have made it past the curve. Personally, I would put a lot of the blame on the documentation which, to my tastes, is a bit too academic; terms like transclude abound. `nuff said.

At the ASF we have a board with 9 Directors, and a couple of hundred officers, give or take. Every month, an agenda is created that 9 Directors review and 50 plus officers contribute to. The artifact is a single file, stored in subversion. Essentially, a superset of the files you can find posted here. The relevant additions include comments and pre-approvals.

As you might imagine, this many people updating a single file, many up to the end of the very end of deadline of the start of the meeting, we have conflicts. To cope, I wrote a tool that allowed me to review individual reports one at a time, collect up comments and pre-approvals, and apply them automatically all at once. In the “production” version of this application, the logic primarily resides on the server, and moving to the next report requires a round trip.

I’m currently rewriting it to move nearly all the logic to the client. You can see the work in progress here.

Guided walkthrough the code

The application starts by downloading the agenda serialized in a pre-processed JSON format, as well as all of the “pending” operations, also serialized in JSON. Both persist on the server as flat files. Once downloaded, traversing to the next report is as easy and as seemless as using most of the HTML5 slide scripts (my current favorite of which is reveal.js, but I digress).

As on the server, processing a page transition involves a router, and you can see my routing as a case statement at the top of app.js. The syntax is Ruby, with a dash of DSL, and sprinkling of JavaScript semantics, but I’ll come back to that.

I also currently include all of the controller logic in the same file. If you disapprove, blame me not Angular.js as you can break this out as you like. As it stands, I have a few large controllers, and several small controllers. As to the former, I intend to go back and refactor soonish.

This brings me to my primary beef with the Angular.js documentation. It would have been helpful to me to be made aware of sooner that while a typical server application would have a single controller serve a dozen or even dozens of pages, a typical angular.js page will have several, or perhaps even dozens of controllers active at any one time. But now you know this too, so you have a head start over where I was at but a few short weeks ago.

Back to the application.

A controller in Angular.js is associated with a DOM node, and therefore all of its children. A controller associated with one of those child nodes inherits instance variables and methods from all controllers associated higher in the tree. A common pattern, therefore, would be to have a controller that controls only one button. Such a controller would have access to everything in the enclosing form.

Let’s walk through an example. One the pages in my application shows comments. On that page is a button that toggles whether or not to show comments that you have seen before. First, here’s the HTML markup for the button itself:

<button 
  class="btn btn-primary"
  ng-show="seen_comments" 
  ng-controller="ToggleComments"
  ng-click="click()"
  > seen</button>

The class attribute references classes defined by Bootstrap.

The ng-show attribute indicates that this button is only to be shown if the value of seen_comments is true. That value is set by a controller.

As you might have figured out, the ng-controller attribute indicates which controller is to handle this DOM Node.

Similarly, ng-click indicates which method on that controller to invoke when the button is clicked.

Finally, the button contains a reference to a label variable that toggles between the values of hide and show. With that, lets move on to the controller:

controller :ToggleComments do
  @label = 'show'

  def click
    broadcast! :toggleComments, (@label == 'show')
    @label = (@label == 'show' ? 'hide' : 'show')
  end
end

This code starts out by setting the instance variable @label to show.

A single instance method named click is defined which, when run, broadcasts to every active controller a message containing a true or false value, and then proceeds to toggle the value of the label.

The important thing to be aware of by this point is that even if there were no other code in place, what you have seen is enough to toggle the text on the button.

Before proceeding, here is the corresponding JavaScript, exactly as it is produced verbatim, for those who are interested in such:

AsfBoardAgenda.controller("ToggleComments", function($scope, $rootScope) {
  $scope.label = "show";

  $scope.click = function() {
    $rootScope.$broadcast("toggleComments", $scope.label == "show");
    $scope.label = ($scope.label == "show" ? "hide" : "show")
  }
});

The entire source for the comments view is found in partials/comments._html. This contains not only the button you have seen already, but also another button. Above these is text that shows up if there are no comments. And above that, a loop that shows comments from selected agenda items. Let’s dive into that selection:

ng_if: 'item | show : {seen: pending.seen, toggle: toggle}'

This code takes the item and passes it through a filter called show, passing that filter a hash containing two values obtained from the relevant controller. Here’s the definition of the filter itself:

filter :show do |item, args|
  return false unless item.comments
  return true if args.toggle
  return args.seen[item.attach] != item.comments
end

Pretty straightforward stuff:

Now, let’s look at how values defined in the controller are set:

on :toggleComments do |event, state|
  @toggle = state
end

show = filter(:show)
watch 'agenda.update + pending.update' do
  $rootScope.unseen_comments =
    @agenda.any? { |item| return show(item, seen: @pending.seen) }
  $rootScope.seen_comments = !Object.keys(@pending.seen).empty?
end

The on statement responds to the broadcast that was shown above, and sets an instance variable based on what was passed. The watch statement watches for changes in the value of an expression, and when it changes it will recompute unseen_comments and seen_comments using the exact same filter used in the view. If you look closely, this code makes use of Object.keys which is part of the JavaScript object model, in the midst of a Ruby expression. There are tradeoffs involved here, but the key point here is that seemless access to the full JavaScript programming model is available.

At this point, we’ve explored views and controllers (and routing and filters). Now let’s briefly touch on models.

A model in Angular.js is a simple class, and often a singleton in that all methods are class methods and all variables are class variables. The Agenda class, for example, defines a self.refresh method that does a $http.get, and calls Agenda.put with the result it receives. The point is that such classes can contain arbitrary logic.

Two more stops on this brief tour. First the entire controller for the other button on this page:

controller :MarkSeen do
  @disabled = false
  def click
    @disabled = true

    # gather up the comments
    seen = {}
    Agenda.get().forEach do |item|
      seen[item.attach] = item.comments if item.comments
    end

    data = { seen: seen, agenda: Data.get('agenda') }

    $http.post('json/markseen', data).success { |response|
      Pending.put response
    }.error { |data|
      $log.error data.exception + "" + data.backtrace.join("")
      alert data.exception 
    }.finally {
      @disabled = false
    }
  end
end

This code initially sets the button to not disabled (inexplicably, the Angular.js core team refuses to define an ng_enabled attribute).

When the button is clicked, the button itself is initially disabled. Then the seen comments are then gathered up from the client model. The name of the agenda file is added, and the result is serialized and sent to the server via a $http.post.

If a successful response is received, it the pending values are updated with the values from the server. If an error is received, it is logged and an alert is shown. Either way, the button is re-enabled.

Note the complete lack of direct reference to jQuery in any part of this scenario. Angular.js will work well with jQuery if present, so that’s not an issue, but the point is that the framework will take care of the DOM manipulation so that you don’t have to.

The final stop on this brief, but wirlwhind, tour is the server side of this operation, namely markseen._json:

pending = Pending.get

pending['agenda'] = @agenda
pending['seen'] = @seen

Pending.put(pending)

_! pending

This logic fetches the Pending model, updates to entries in the hash based on data sent by the client, puts the model back, and then returns the updated model (which generally contains other values which weren’t updated by this specific operation) back to the client. For completeness, pending.rb contains the logic serializing and deserializing the server model (in this case, using YAML).

Recap

We have a model, view, and controller on the client, seemlessly interacting with the model, view, and controller on the server. Everything (except for a small stylesheet) is defined using Ruby syntax, and is converted to HTML, JavaScript, or directly executed as appropriate. While I chose Ruby, other choices could obviously be made. The Angular.js framework can also be used directly (and browing the generate JavaScript would help show you how to do this), at a cost of some additional learning curve (things like dependency injection, which are taken care of by my mapping filters for angular.js).

The point here being that there are good frameworks out there that do client side MVC. These frameworks (quoting directly from Tim’s original post):

embody[…] a lot of history and hard-won lessons. Crucially, for most of the things you’d want to put in a UI, there’s usually a single canonical solid well-debugged way to do it, which is the top result for the appropriate question on both Google and StackOverflow.

Finally, if you are interested in the Ruby code, you are encouraged to look into wunderbar, it’s associated tutorial, and ruby2js.