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