Tuesday, March 6, 2012

MVC History in Dart

‹prev | My Chain | next›

At this point, I have pushState and popState in Dart working well enough that, if I clear the browser history and navigate to a page, the correct page is routed and rendered:


This works entirely because of a call to startHistory(), which adds a listener to the popState event:
startHistory() {
  window.
    on.
    popState.
    add((event) {
      var page_num = window.location.hash.replaceFirst('#', '');
      render(page_num);
    });
}
Even when the page first loads, the popState event fires, at which point my page handler kicks in to extract the page number parameter from the URL hash (i.e. "Fourty-Two" in "http://example.com/page#Fourty-Two"). I can then route to the appropriate resource in my application.

The problem here is my handling of history is hopelessly coupled with my application routing. Worse yet, the routing is hard-coded inside the handling of the browser history. If I wanted to add a greetings route (e.g. #howdy/Bob), then I would have to add it directly to the history handler.

No matter what, the history mechanism is going to need to know what to call on popState. Instead of hard-coding it though, it should be injected. But first, the startHistory() function is going to need to be a class that can encapsulate the list of routes:
class HipsterHistory {
  static startHistory() {
    window.on.popState.add(_checkUrl);
  }

  static _checkUrl(_) {
    var page_num = window.location.hash.replaceFirst('#', '');
    render(page_num);
  }
}
If I then change the start-up call to HipsterHistory.startHistory(), then everything continues to work.

Still, _checkUrl() is hard-coded to render the numbered page route. Since I will want to be able to route to multiple destinations, I will want a list of routes. I can then iterate over on each popState:
class HipsterHistory {
  static List routes;

  static startHistory() {
    routes = [[new RegExp(@'^[A-Z][-\w]+$'), render]];

    window.on.popState.add(_checkUrl);
  }

  static _checkUrl(_) {
    var fragment = window.location.hash.replaceFirst('#', '');

    var matching_handlers = routes.
      filter((r) => r[0].hasMatch(fragment));

    if (matching_handlers.isEmpty()) return;

    var handler = matching_handlers[0];
    handler[1](fragment);
  }
}
Here, I am using a regular expression (@'^[A-Z][-\w]+$') to only route when the URL matches something like "One", "Three", or "Forty-Two". The _checkUrl() no longer hard codes the route, but iterates over the know routes. If it finds a matching route, the handler is invoked.

Everything still works at this point, but I still need to inject that route into the list of handlers known to the History mechanism. A class method route() to add an individual route and callback pair should do:
class HipsterHistory {
  static List _routes;

  static get routes() {
    if (_routes == null) _routes = [];
    return _routes;
  }

  static route(route, fn) {
    routes.add([route, fn]);
  }

  static startHistory() {
    window.on.popState.add(_checkUrl);
  }

  static _checkUrl(_) {
    var fragment = window.location.hash.replaceFirst('#', '');

    var matching_handlers = routes.
      filter((r) => r[0].hasMatch(fragment));

    if (matching_handlers.isEmpty()) return;

    var handler = matching_handlers[0];
    handler[1](fragment);
  }
}
With that, my start code becomes:
main() {
  HipsterHistory.route(new RegExp(@'^[A-Z][-\w]+$'), render);
  HipsterHistory.startHistory();
}
And everything still works. I could do with an easier API to add multiple routes, but this is a pretty decent start on a history mechanism for my MVC library.

Day #317

No comments:

Post a Comment