October 12, 2011

A First Pass at UI-RPC

Based on the ideas I laid out the other day, I've taken a first pass at something I'm calling UI-RPC. This is a simple proof-of-concept project meant to demonstrate the use of HTML5's postMessage() API for event handling and inter-widget communication.

Code: https://github.com/baspete/UIRPC
Live: http://dev.basdesign.com/UIRPC/

The single-page app displays a list of congresspersons matching some simple filter criteria. Clicking on a congressperson's photo produces some additional information about them.

The html page itself is just a skeleton, responsible for loading CSS, javascript libraries, and laying out the basic page structure. The actual functionality is divided among two types of javascript objects: widgets and workers. The (admittedly arbitrary) distinction between the two is that widgets have a presentation component, and workers don't. Widgets are added into the markup like so:


Note that those divs are empty. That's because, near the end of the page markup, we call a javascript function, createWidgets(), which will crawl the DOM, find the various widget containers, instantiate an appropriate widget object for each container, and fire that object's init() method:

UIRPC.createWidgets();

At the same time, a different javascript function, createWorkers() is passed an array of workers which should be instantiated on this page.

UIRPC.createWorkers(["congress","facebook","twitter"]);

Finally, an event dispatcher is initialized, which events and helpers use to communicate with each other. This is a special type of helper, and looks for a map which associates one or more procedures with an event (for convention, events are capitalized and procedures are camel-cased):

UIRPC.events = {
    FILTER_CHANGED: ["getLegislators","showRetrievingData"],
    LEGISLATORS_CHANGED: ["showLegislators"],
    REQUEST_DETAILS: ["getCommittees","getFacebookInfo","getTwitterInfo"],
    GET_COMMITTEES_RESULTS: ["showCommittees"],
    GET_FACEBOOK_INFO_RESULTS: ["showFacebookInfo"],
    GET_TWITTER_INFO_RESULTS: ["showTwitterInfo"]
  };

A widget or helper registers a procedure like so:

pmrpc.register({
    publicProcedureName: "showLegislators",
    procedure: function (data, options) {
      showLegislators(data, options);
    },
    isAsynchronous: true
  });

Widgets and helpers are written as closures, and each publicly registered procedure corresponds to one of the module's private methods (in this case, "showLegislators").

When a widget or helper wants to fire an event to the event dispatcher it uses the pmrpc.call() method. For example:

pmrpc.call({
      destination : window,
      publicProcedureName : "event",
      params : {
        data: {
          eventName: "FILTER_CHANGED",
          data: getFilterCriteria(),
          options: null
        }
      }
    });

In this case, the FILTER_CHANGED event is called with the data returned by the widget's private "getFilterCriteria()" method.

The easiest way to see this in action is to load the page and watch the javascript console. The event dispatcher will log to the console every time it dispatches an event to a procedure:

dispatching  FILTER_CHANGED  to  getLegislators  with data:  Object  and options  null
dispatching  FILTER_CHANGED  to  showRetrievingData  with data:  Object  and options  null
dispatching  LEGISLATORS_CHANGED  to  showLegislators  with data:  [Object, Object, Object]  and options  null

As you can see, the FILTER_CHANGED event is being passed to the getLegislators and showRetrievingData procedures, and then the LEGISLATORS_CHANGED event is passed to the showLegislators procedure.

Clicking on a legislator's picture produces something like:

dispatching  REQUEST_DETAILS  to  getCommittees  with data:  Object  and options  Object
dispatching  REQUEST_DETAILS  to  getFacebookInfo  with data:  Object  and options  Object
dispatching  REQUEST_DETAILS  to  getTwitterInfo  with data:  Object  and options  Object
dispatching  GET_FACEBOOK_INFO_RESULTS  to  showFacebookInfo  with data:  Object  and options  Object
dispatching  GET_TWITTER_INFO_RESULTS  to  showTwitterInfo  with data:  [Object, Object, Object]  and options Object
dispatching  GET_COMMITTEES_RESULTS  to  showCommittees  with data:  [Object, Object, Object, Object, Object]  and options  Object

Issues


Clearly, there are a couple of problems with this framework as it currently exists:

  1. The event mapping is too simplistic. For real-life applications, the current method of mapping events to an array of procedures won't be sufficient--a developer will need to be able to apply conditional logic, failovers, etc. But the idea seems sound, and this issue could be addressed through the use of a real map instead of just an array.
  2. There's markup in my javascript!. Yup, and this sucks just as bad as having markup hard-coded into the page. I think this is the most important thing to fix here. There are templating systems out there, but I'm not sold on any of them. I'm thinking about the way backbone.js does it may be a good place to look for ideas.
  3. There's a lot of duplicated code, particularly the procedure registration part. But we're using the javascript module pattern here, and the next logical step seems to be creating specific modules/helpers by extend()ing generic module/helper modules.

Overall, I think this might be a useful pattern. At this point it's hard to tell if it meets the criteria of getting front-end developers to think first about functionality. That will take another project.

[edit - changed github repo]

No comments:

Post a Comment