Friday, March 9, 2012

Custom Routing Events in Dart

‹prev | My Chain | next›

Dart is a pretty cool little language—especially for something at version 0.07. But being at 0.07, there are numerous rough edges and things that just don't work. One of things that does not work is setting custom events on DOM elements. Tonight, however, I hope to do something a little different—custom router events.

I have the router in my Hipster MVC framework working to the point that I can initiate an app like so:
main() {
  new MyRouter();
  HipsterHistory.startHistory();
}

class MyRouter extends HipsterRouter {
  List get routes() =>
    [
      ['page/:num', pageNum]
    ];

  pageNum(num) { /* ... */ }
}
If that seems somewhat Backbone.js-like, that is because I ripped it off almost entirely from Backbone. But Backbone supports router events similar to:
main() {
  HipsterRouter app = new MyRouter();

  app.
    on['route:page'].
    add((num) {
      print("Routed to page: $num");
    });

  HipsterHistory.startHistory();
}
In other words, I would like my router to dispatch a "route:page" event, and I would like to be able to add a listener for that event like I did above.

First, I need to be able to tell the router that a particular route generates a route with a name like "page". For that, I add a third option to the routes in MyRouter:
class MyRouter extends HipsterRouter {
  List get routes() =>
    [
      ['page/:num', pageNum, 'page']
    ];
  // ...
}
Then, in the _initializeRoutes() method of the base-class, I use that third parameter to name the event that will be generated:
class HipsterRouter {
  // ...
  _initializeRoutes() {
    routes.forEach((route) {
      HipsterHistory.route(_routeToRegExp(route[0]), (fragment) {
        Match params = _routeToRegExp(route[0]).firstMatch(fragment);
        String event_name = (route.length == 3) ? "route:${route[2]}" : "route:default";
        // ...
      });
    });
   }
   // ...
}
At some point in the future, I can either reflect on the function name (e.g. pageNum()) or extract the event name from the URL fragment. But, for now, I assume that a lack of a third parameter indicates that the calling context does not much care about the event, so I dump it into the "route:default" event space.

With an event name chosen, I need to dispatch the event. As I found last night, Dart cannot yet splat / apply arguments so I have to dispatch events the same way that I invoked the supplied callback. That is, I have to do it ugly:
class HipsterRouter {
  // ...
  _initializeRoutes() {
    routes.forEach((route) {
      HipsterHistory.route(_routeToRegExp(route[0]), (fragment) {
        Match params = _routeToRegExp(route[0]).firstMatch(fragment);
        String event_name = (route.length == 3) ? "route:${route[2]}" : "route:default";
        if (params.groupCount() == 0) {
          route[1]();
          this.on[event_name].dispatch();
        }
        else if (params.groupCount() == 1) {
          route[1](params[1]);
          this.on[event_name].dispatch(params[1]);
        }
        // ...
      });
    });
   }
   // ...
As ugly it is to manually call the same function with different arity, it works.

With that, I know how I want to dispatch string-based events, and I already know from my main() entry point how I want to subscribe to them. All that remains is on property that will support dispatching events and subscribing listeners.

That actually turns out to be fairly easy:
class RouterEvents implements Events {
  HashMap<String,RouterEventList> listener;
  RouterEvents() {
    listener = {};
  }

  RouterEventList operator [](String type) {
    listener.putIfAbsent(type, _buildListenerList);
    return listener[type];
  }

  _buildListenerList() => new RouterEventList();
}
The constructor initializes the listeners instance variable to an empty hash. The keys of that hash will be event names like "route:page" and the keys will be the list of event listeners.

The cool bit in there is the square brackets operator method which returns the list of event listeners:
  RouterEventList operator [](String type) {
    listener.putIfAbsent(type, _buildListenerList);
    return listener[type];
  }
Before returning the listener list, I use the very nice, very confident putIfAbsent(). If a key, like "route:page" does not exist, putIfAbsent() will add it with the value returned by the second argument—a simple builder method.

I also have to define RouterEventList and RouterEvent classes, but those are quite similar to the Collection event classes that I have previously defined.

With that, I have an on property for my Router class:
class HipsterRouter {
  RouterEvents on;

  HipsterRouter() {
    on = new RouterEvents();
    this._initializeRoutes();
  }
  // ...
}
And the event listener back in main():
main() {
  HipsterRouter app = new MyRouter();

  app.
    on['route:page'].
    add((num) {
      print("Routed to page: $num");
    });

  HipsterHistory.startHistory();
}
Is actually generating print() output in the console:


I am quite pleased with that implementation. Dart's support for operator methods is a nice win here. I love the putIfAbsent() method on HashMaps. I plan on using that a lot as I continue to explore Dart.

Day #320

No comments:

Post a Comment