read

Previously we looked at a very simple sample application that uses responders to manage application state. Now that our responder-fu is strong, we can take a further step of using SproutCore routes to manage history via states.

The Underlying Technology

As amazing as SproutCore is, it did not in fact invent the methods it uses to handle history for single-page applications. Applications like Gmail have been doing this for quite a while, and it basically involves setting the browser’s location with JavaScript, then monitoring the location and taking appropriate action if the user has navigated to a previous (or subsequent) location in the browser history.

In particular, these single-page applications use the location.hash that’s available in JavaScript, which provides the portion of the current URL that follows the ‘#’ character. For instance, if you’re at http://example.com#inbox, location.hash will return “#inbox”. If there is no ‘#’ in the current URL you’ll just get and empty string. Here’s how SproutCore handles history, as illustrated by the Routes sample app:

Registering a route

In order to make use of routes in SproutCore, you need to register any route changes you want to watch for. This is done by providing a route string that is matched against the browser location, along with a target object and method you want called on that object. Since route changes will often coincide with state changes, it makes sense to use responders to handle them. The Routes sample application has a superstate, Routes.states.root, that all of the other states are substates of, so this is the responder that is registered:

// main.js
SC.routes.add(':number', Routes.states.root, 'route');

Now that this route has been registered, any time the browser location hash changes the route method in Routes.states.root will be called, with a hash that has a number property consisting of everything to the right of the ‘#’ character in the URL. SproutCore will now monitor the location hash for changes that match this route, either in an event-based way for browsers that support it, or by using a timer.

Setting your location

You set a route in SproutCore by setting the location property for SC.routes, for instance SC.routes.set('location', 'here'). In the Routes sample app, when a change in the location hash occurs and the route method in SC.states.root is called, that method simply passes the number property that was captured to the go method:

// responder.js
route: function(route) {
  this.go(route.number);
}

In the go method, the location is set to the route number that is passed in, and as a nice touch the browser title is also updated to reflect the current route number. Finally, the state is set with Routes.makeFirstResponder.

// responder.js
go: function(number) {
  ...
  SC.routes.set('location', number);
  document.title = "Routes: %@".fmt(number);
  Routes.makeFirstResponder(Routes.states[number]);
}

Following route and state changes

Now that they key players have been identified, it may be useful to follow the flow of events when actually interacting with the app. Assume we’re running sproutcore-samples at http://localhost:4020.

Immediately upon entering the app by browsing to http://localhost:4020/routes, the route that was set by SC.routes.set above will notice the initial URL as a “change”, call the route method in SC.states.route, and in turn enter the go method. Since the location hash starts off empty, the following code in go will default to the one route, and then set the location hash, browser title and state to one:

// responder.js
if (!Routes.states.hasOwnProperty(number)) {
  number = 'one';
}

SC.routes.set('location', number);
document.title = "Routes: %@".fmt(number);
Routes.makeFirstResponder(Routes.states[number]);

So the URL that we intially see once the app is done loading is http://localhost:4020/routes#one. Now let’s say we click on another button, three for instance. Looking in resources/main_page.js we can see that all of the buttons have go as their action:

// resources/main_page.js
threeButton: SC.ButtonView.design({
  layout: { top: 27, left: 243, width: 81 },
  title: 'three',
  action: 'go',
  isEnabledBinding: SC.Binding.transform(function(state) {
    return state !== Routes.states.three;
  }).oneWay('Routes.firstResponder')
}),

Currently the state is Routes.states.one, however when we look there we see that this state doesn’t define the go method itself. Instead, all of the substates simply define Routes.states.root as the nextResponder:

// responder.js
Routes.states.one = SC.Responder.create({
  nextResponder: Routes.states.root
});

This is what makes these substates of Routes.states.root, so the go call won’t be found in the first responder, causing it to go up the chain to Routes.states.root. This will in turn change the current URL to http://localhost:4020/routes#three, update the page title and set the state to three. Importantly, each time the current route is changed, those changes are reflected in the browser history. We could continue clicking buttons like this as long as we’d like, accumulating a history of being in the different locations that each button correspond to.

If we next hit the back button in our browser, that will change the browser URL back to http://localhost:4020/routes#one. Because of the route we added, this will trigger calling go in the root responder, and in turn the page title and state being updated to one.

Wrap-up

The Routes sample app provides a simple, albeit contrived, example of how routes and states allow us to use history in our apps. In order to apply this to a real app, you would need to consider what states in your app you’d like to make available in the browser history, and how you would accomplish transitioning to those states from essentially any other state in your app. I only know of one real app that uses responders and routes for history management, SproutCore Tests. There are some interesting nuances to how it works with routes, so it’s certainly worth a look if you have the time.

Blog Logo

Bruz Marzolf


Published

Image

Destructured

Back to Overview