Wednesday, March 7, 2012

MVC Routing with Dart

‹prev | My Chain | next›

One of the many things that I love about Backbone.js is the simplicity of the router. Specifying a list of route / callback pairs is brilliantly easy and still quite powerful. Since I have already shamelessly ripped off most of Backbone.js in my Dart-based Hipster MVC, I might as well go all the way and rip off the router as well...

In fact, I already have a pushState based history mechanism, so the router should be straight-forward. "Should" is always a fun word.

With my history implementation, I can manually specify routes such as:
HipsterHistory.route(new RegExp(@'^page/[-\w]+$'), pageNum);
In Backbone, this might be something like:
var MyRouter = Backbone.Router.extend({
  routes: {
    "page/:num": "pageNum"
  },

  pageNum: function(num) { /* ... */ }
});

new MyRouter();
I don't know that I can get the placeholder stuff working tonight, but I would like to get a minimalist router going. So first up, I replace the manual HipsterHistory.route() call with a new router object:
main() {
  new MyRouter();
  HipsterHistory.startHistory();
}
As for MyRouter, I pull in the existing pageNum() function as a method:
class MyRouter {
  List routes;

  pageNum(num) {
    var el = document.query('body');
    el.innerHTML = _pageNumTemplate(num.replaceFirst('page/', ''));
  }
}
I start by defining my routes in the constructor as:
class MyRouter {
  List routes;

  MyRouter() {
    routes = [
      ['page', this.pageNum]
    ];

    _initializeRoutes();
  }

  pageNum(num) { /* ... */ }
}
It is then the responsibility of the _initializeRoutes() method to assign these to HipsterHistory.route() appropriately:
class MyRouter {
  List routes;

  MyRouter() {
    routes = [
      ['page', this.pageNum]
    ];

    _initializeRoutes();
  }

  _initializeRoutes() {
    routes.forEach((route) {
      HipsterHistory.route(new RegExp(route[0]), route[1]);
    });
  }

  pageNum(num) { /* ... */ }
}
And that still works! Er... I mean of course that works. I had no doubt.

With that working, I decide to push my luck. Instead of matching the page route and forcing pageNum() to extract parameters:
  pageNum(num) {
    var el = document.query('body');
    el.innerHTML = _pageNumTemplate(num.replaceFirst('page/', ''));
  }
I would rather specify the route with a place holder so that I can supply pageNum(num) with an actual num and not the entire URL fragment:
class MyRouter {
  List routes;

  MyRouter() {
    routes = [
      ['page/:num', this.pageNum]
    ];

    _initializeRoutes();
  }
  // ...
}
The first thing that I will need to do is convert the placeholder to a regular expression that extracts the values that would be there. Something like ([^\/]+) (capture one or more characters that are not slashses) ought to do:
// ....
  _initializeRoutes() {
    routes.forEach((route) {
      HipsterHistory.route(_routeToRegExp(route[0]), route[1]);
    });
  }

  _routeToRegExp(matcher) {
    var regex = matcher.replaceAll(new RegExp(@':[^\/]+'), '([^\/]+)');
    return new RegExp(regex);
  }
// ....
Only that throws an error:
Exception: Unimplemented String.replaceAll with RegExp
Stack Trace:  0. Function: 'StringBase.replaceAll' url: 'bootstrap_impl' line:2039 col:7
 1. Function: 'MyRouter._routeToRegExp@676514c' url: 'file:///home/cstrom/repos/dart-book/book/includes/push_state/main.dart' line:26 col:35
 2. Function: 'MyRouter.function' url: 'file:///home/cstrom/repos/dart-book/book/includes/push_state/main.dart' line:21 col:42
 3. Function: 'GrowableObjectArray.forEach' url: 'bootstrap_impl' line:1256 col:8
 4. Function: 'MyRouter._initializeRoutes@676514c' url: 'file:///home/cstrom/repos/dart-book/book/includes/push_state/main.dart' line:20 col:19
 5. Function: 'MyRouter.MyRouter.' url: 'file:///home/cstrom/repos/dart-book/book/includes/push_state/main.dart' line:16 col:22
 6. Function: '::main' url: 'file:///home/cstrom/repos/dart-book/book/includes/push_state/main.dart' line:4 col:3
Eke! Dart has not implemented replacing regular expressions in strings?! Say it ain't so!

In fact it looks as though it is so. I get the same error when I try replaceFirst(). Dang. That'll teach me to quit when I'm ahead.

With a working, if somewhat limited Router in place, I call it a night here. I will come up with a workaround for this tomorrow.


Day #318

No comments:

Post a Comment