Quantcast
Channel: JavaScriptMVC Forum
Viewing all articles
Browse latest Browse all 3491

"nullrouter": can.route without routing

$
0
0
This is one part "hey, things didn't quite work right", one part "here's a hopefully-not-too-ugly workaround", and one part braindump. We're using canjs 2.0.3.

This is rather long; there's nothing too important here unless you're dealing with routes and window.history / session management.

--

We recently tried to refactor our app's routing to use the can/route/pushstate plugin instead of the default hashchange, and we ran into a few issues. As near as I could tell, most of our issues resulted from the way can.route itself works, and not anything about the pushstate plugin itself -- but the end result was that we couldn't get a good user experience using can/route/pushstate.

The main issue appears to be that can.route's observable/map properties are *synchronized* with the url string, such that modifying one will modify the other. This makes a lot of sense with the hash, but with the url as a whole we ended up with some awkward results:
  • Initializing routing sets properties on can.route, which triggers a pushstate, so immediately on page load there's an extra history entry: if the user clicks the back button right away, nothing happens. They have to click it twice.
  • The above behavior occurs twice when clicking a link for a route: the plugin does its own pushstate and the resulting can.route changes trigger more pushstates. We ended up with two duplicate history entries (3 total) for each link click, so if the user wants to go back they have to click 2-3 times per route visited.
  • If the user presses the back button enough to visit a previous route then can.route's properties change, which triggers a pushstate, causing their 'forward' history to be erased.

I spent some time trying to modify the pushstate plugin to fix these issues, but most of these behaviors seem innate to the way can.route works -- the way it needs to work for hashchange to behave nicely causes pushstate to behave awkwardly, and vice versa.

--

So, here's the workaround we used, in case anybody else runs into these same issues. This is a rather hacked-up solution, but it ended up working perfectly for our needs.

The main idea was to use can.route's url recognition, parsing, and generation features without letting it actually modify the window history. We manage the history ourselves now, and our code pushes state to can.route (instead of the other way around), except on page init and popstate. Here's what we did:

1. We created a new plugin for routing, which we called "nullrouter".
  • We copied pushstate.js to nullrouter.js, and made it set can.route.bindings.nullrouter instead of can.route.bindings.pushstate.
  • We replaced bind, unbind, and setUrl with no-ops, and removed anchorClickFix. This left us with matchingPartOfURL, the local cleanRoot function, and the normal params for routing (root, paramsMatcher, and querySeparator).
  • At this point, using the console, we were able to define, param()/url(), and deparam() string routes and configs using can.route, but neither the url nor can.route's properties were changed. Additionally, we were able to set can.route's properties (via attr()) and modify window.history directly, but they were now independent -- altering one did not trigger any changes in the other.

2. Now we could use can.route for parsing and generating urls, while setting the url and history ourselves. We made some functions for this, and put them into the controller which manages our whole app.
  • In the controller where all our routes are defined, a persistent top-level control whose main duty is to create other controls, we created a gotoPage function and a showPage function. Both take a config (a deparam'd route):
  1. gotoPage: function(config) {
  2.       // We update the browser history and set can.route's properties at the same time.
  3.       // (can.route's properties are handled by showPage)
  4.       var fullUrl = can.route._call('root') + can.route.param(config);
  5.       window.history.pushState(config, pageTitle, fullUrl);
  6.       this.showPage(config);
  7. },
  8. showPage: function(config) {
  9.       // Note that we only write to can.route, we don't read or respond to its values.
  10.       can.route.attr(config, true);
  11.       // and then we respond to config: render templates, set up controls, etc
  12. },
  • We added an event listener for link clicks; a barely-modified version of can/route/pushstate plugin's anchorClickFix. Instead of letting it call window.history.pushstate(), we made it call this.gotoPage(curParams);
  • Within init, we enable routing and immediately showPage. This avoids pushing a redundant history entry on page load.
  1. can.route.ready();
  2. var initialRoute = can.route.attr();
  3. this.showPage(initialRoute);
  • And we do the same thing on window.popstate. This avoids rewriting the forward history when the user clicks back.
  1. can.bind.call(window, 'popstate', function() {
  2.        var currentUrl = can.route._call('matchingPartOfURL');
  3.        self.showPage(can.route.deparam(currentUrl));
  4. });

From here everything worked for our use cases. Links updated the page and its history like normal, and the browser history behaved as expected while going backwards and forwards between pages. There was one history entry per page visited.

Nested controls which needed to trigger page routing (e.g., to redirect after creating a new resource) could call pageController.gotoPage(...) -- the lower-level controls have a reference to that persistent top-level control. There's a 'rule' that nothing else in the system is allowed to call window.history.pushstate or touch can.route.attr().


With the above changes made, it appears that we're still getting all the normal route events and could make a controller listen to "foo/bar route", but in our app everything is driven by the config for the route -- which we already have in showPage -- so we haven't tested that extensively.

I can't guarantee that this will work or will be useful for anybody else, but I hope it's helpful. Using 'nullrouter' to turn can.route into a library that we boss around, instead of one which generates events that we obey, let us solve each of the issues we ran into initially.


Viewing all articles
Browse latest Browse all 3491

Trending Articles