In the last post, we walked through the steps to get our new Ext JS 5 application setup and running. Now we can start laying the foundation for other areas of the new version that we’d like to explore.

Part of this foundation will be to implement the new routing functionality that has been included with Ext JS 5. If you’ve used Sencha Touch before, this will be very familiar to you. If you’ve never seen it, I think you’ll find it pretty simple to pick up.

We need to get routing setup, however, because we’ll use it throughout the other examples that we’ll explore in future installments. The routing will simply make this process easier…so let’s get started!

Source and Demo

The Main Idea

So what is routing? In the simplest sense, it’s a way for you to leverage the browser’s normal history stack to maintain application state within an Ext JS application. Since your application is, by default, only one page, the browser’s back and forward buttons don’t do much good out of the box. With routing, however, you can wire particular states of your application to the browser’s history stack, allowing you to create a richer user experience by allowing for movement between application state, deep linking to particular aspects of the application, etc.

For example, let’s imagine that you have a grid with a list of users that displays detailed information in a panel when each user is clicked. Using routing, you could modify the hash (or fragment identifier) of the your application’s URL when a user is selected, such as:

http://myapp.com/#users/SOMEID

If you were to paste this URL into a new browser window, your routing configuration could instruct your app to compose itself to a particular configuration that would:

  • Render the user grid
  • Load the user with the specified ID (SOMEID)
  • Select the loaded user and display their information in the “detail” panel

Pretty cool, huh? But even more, you can leverage this to provide the full “back and forward” experience for your users. So let’s say that you have a link to display a user management section:

http://myapp.com/#users

This will display a grid of users and load a page of data

Now let’s imagine that our previous example of a particular user is selected, which modifies the URL to:

http://myapp.com/#users/SOMEID

As before, this will load the individual user detail.

But now, if the application user clicks the browser’s back button, we can restore the state of our users grid to its original state and clear the detail display. Now, not only do we have deep linking within out app, but we also have a very user-friendly and intuitive way for our users to interact with the app in the same way that they would any other web site.

Routing in Action

Ok, so we know what we want to do. Let’s get to it!

Before you dive in head-first, it’s important to understand that IF you decide to use routing, you need to begin to think about how you structure your app in terms of the implications of routing. Take, for example, our user grid: in an app without routing, you might handle loading the “detail” of a selected grid record by simply responding to the itemclick or select events of the grid and loading the target view with data. When you’re routing, however, you need to approach it a bit differently. Yes, you’ll still be dealing with the same events. However, instead of these events themselves doing the final “thing”, a routing model will have these events setting application state…and it will be the result of this application state change that does the final “thing.” To illustrate, let’s look at our user grid.

Without routing, you might load a “detail” view of a grid row like so:

onItemClick: function( view, record, ...) {
   this.getView().down( '#detail' ).update( record.getData() )
}

While this obviously works very nicely, it does NOTHING for application state. If I reload the web application at this point, I have nothing to hook on to in order to reload the grid, select the same record, and display the detail view.

With routing, we need to think a bit differently. Again, instead of doing the “thing” in our event handler, let’s do something a bit different:

onItemClick: function( view, record, ...) {
   this.redirectTo( 'users/' + record.get( 'id' ) );
}

Ah, something different! Instead of doing the “thing” (loading the detail view) in this event handler, we are calling the routing-specific method redirectTo, instructing the router that we want to set an application state with the identifier of ‘users/:SOMEID’.

With our new code in place, a click of the grid item will no longer load the detail view with data (we’ll wire that up later). Instead, it will merely execute this code which will result in the hash of our application’s url being changed from

http://localhost:1841/

to

http://localhost:1841/#users/3

The part in bold has been added to the URL, and the underlying history manager within Ext JS has been updated to understand this as the application’s current “state”.

What this means, in practical terms, is that your routing code needs to be smart enough to not only respond to events that occur and set application state appropriately, but to also anticipate the requirements of the application to get into that same “state” from any particular route.

For example, let’s say that during the course of our application flow, we set the application state to

http://localhost:1841/users

This state should display our users grid. But now let’s assume that we copy this URL into a new browser window. If we want our state to be functional, it needs to be self-aware and capable of rebuilding the application from scratch in order to get back to state that it was in our previous browser instance…simply from the fragment passed in the URL. For this example, this would include:

  • Launching the application
  • Rendering the grid
  • Loading the grid with data

Ok, that’s not to bad. But now let’s go deeper. Let’s select a user from grid and display the details. As we already saw, our application state url will look like:

http://localhost:1841/#users/3

This creates a tricky problem with restoring state, however, because we cannot (and should not) depend on the prior existence of the user grid when building this state. So in order to restore this state from scratch (deep-linking), we’ll need to do the following:

  • Launch the application
  • Render the user grid
  • Load a page of data that includes the target user (imaging the implications for a remote store…)
  • Find the user record in the grid
  • Fire the itemclick or select event of the user record
  • Display the user detail

That’s a lot of steps! But again, if you want to maintain proper application state, they are necessary.

NOTE: Depending on the structure of your app, the amount of work you’ll need to do to load a particular state will vary. In this particular example, the grid and the detail view are rendered at the same time, always, so it is definitely necessary that they are both managed if we want proper state for the #users/3 route. If our app were structured differently and the detail view was independently rendered from the grid, we would only have to worry about the detail view (and it’s bound data) in order to maintain that particular state.

The point, ultimately, is that for routing to work effectively, you need to have a good plan of action for how you’re laying out your app, how different components are going to interact with one another, and what your strategy is for recreating each particular state.

What’s the point of all of this, then? It’s not to scare you away from routing :). Rather, it’s a just a friendly heads-up that implementing routing within your application requires some serious forethought and planning. If you really want to be able to provide robust application state management, routing is not something that can just be slapped on top of existing code; rather, it needs to be core to how you plan your applications in general.

NOTE: I made the list of requirements for managing the different “states” of this example deliberately onerous in order to prove the point. Obviously there are simple patterns that can be adopted to abstract a lot of this work so that it can be reused across similarly-structured application states.

Setting Up Routing

Now that we are using the built in routing support of Ext JS to modify our application’s URL, we need to fill in the plumbing to be able to respond to these appropriately. We do this by adding the new routes config to either a regular application controller or one of the Ext JS 5’s new view controllers.

In our app, we have two “states” that we want to deal with:

  • #users  -> When in this state, our main center region of the app should render the “users” grid
  • #users/SOMEID -> When this state, our main center region of the app should render the “users” grid, and the user which matches the SOMEID argument should be selected in the grid AND the detail view should be filled with that user’s data

For the purposes of this example, I’ve created a Users controller which I will use for managing the routing requirements for this section of the application (see notes below about Controllers vs ViewControllers). Here’s the routes configuration:

Ext.define('ExtJS5Explorer.controller.Users', {
    ...
    init: function() {
       this.addRef([{ref: 'UserGrid', selector: 'grid' }]);
       this.callParent();
    },
    routes: {
        'users/:id': {
            action: 'onUserDetail',
            before: 'onBeforeUserDetail'
        },
        'users': {
            action: 'onUsers'
        }
    },
    ...
});

As you can see, the routes config is very simple. You create a very simple configuration of patterns that you’d like to match, and then specify various actions that should be executed on a successful match. In keeping with the examples we’ve talked about above, we have two routes:

users/:id
users

The users route will manage application state for the main user grid, while the users/:id will manage application state for the selection and display of a user’s details.

Route Parameters

While I’m not going into the nitty gritty details of this (you can read more here), routes allow you to pass parameters. This is done simply by adding any named parameter with the following syntax:

:parameterName (colon + whatever parameter name you want)

This pattern works for any number of parameters you’d like to include, and you can mix and match parameters with statics paths as well. For example, the following would have 2 parameters:

users/:group/user/:id

Handling Routes

As seen, route handling is managed very simply by calling a named method in your controller. For routes without parameters, no arguments are passed. If your route includes parameters, these will be passed as arguments to your method, in order. For example, our double-paramtered route might be handled by a function with the following signature:

onGroupUser: function( group, id ) {}

“Before” Handling

There are situations, of course, in which you might want to do some form of test before the actual execution of a route. For example, maybe you want to make sure that the user has appropriate permissions to a section of the site, or that the parameter passed in a particular route still exists. The before configuration allows you to specify a supplemental method that will be executed before the action configured for the route itself. Based on the results of whatever processing you choose to do, you can then resume the route and allow the original action to continue, or abort processing and cancel the route’s action.

The beautiful part of this setup, however, is that action specified in your route will be indefinitely suspended until you explicitly instruct it to resume. This is very nice, for it means that you can make AJAX requests as part of your before handler if you need to, and on the asynchronous callback instruct the action to resume.

The signature of the before handler is very similar to that of the route handler: e..g, if you have parameters, they will be passed positionally as defined in your route’s matching definition. However, an additional parameter (action) will be passed as the final argument. The action is the object responsible for unsuspending the queued action, either by calling resume() or stop(). Here’s the signature for our onBeforeGroupUser handler:

onBeforeGroupUser: function( group, id, action )

Putting it Altogether

Alright, we’ve covered the basics. Let’s put it together.

We’ve already seen the configuration for our routes, so let’s look at the handlers for those routes. First, here’s the handler for the root users route:

/**
 * Handler for matched 'users' route
 * @private
 */
onUsers: function() {
   this.addContentToCenterRegion({
       xtype: 'users'
   });
}

In this method, I’m simply calling a utility method that takes a component configuration and renders it to the main “center” region of my apps’ border layout (you can see the method in the source). Since my grid has local data, I don’t worry to much about loading its data. However, I could easily add a bit more here to manage that if I needed to.

NOTE: In the source, you’ll see that the addContentToCenterRegion() method basically removes existing content from the center region and adds the new instance of the specified component. In 5.0.1, there is apparently a bug which occurs only in production builds which causes an error (see this thread for more details). It has been fixed in 5.0.2, so I’ll update once that is released.

User Detail: Before Handler

Ok, now for the more interesting part: handling the user detail route. First, let’s look at the before handler:

/**
 * Before handler for matched 'users/:id' route
 * @private
 * @param {Number} id
 * @param {Object} action
 */
onBeforeUserDetail: function( id, action ) {
    this.addContentToCenterRegion({
        xtype: 'users'
    });
    // check if record is in grid
    var record = this.getUserGrid().getStore().findRecord( 'id', id );
    if( record ) {
        action.resume();
    } 
    else {
        action.stop();
    }
}

While this before handler is probably not technically necessary, I wanted to put it in to demonstrate how the complexity of routing can start to increase. If you think about it, it’s possible that someone could “bookmark” a particular application state. Over time, that application state could become invalid (i.e., a user gets deleted, deactivated, etc) and we need to be able to handle that.

So in this example, our before handler is checking to make sure that the user requested in the route is a legitimate user. Since my data is local to the user grid itself, I first have to render the grid in order to be able to check its store for the existence of the user (in real life, I should probably just have  stand-alone store that is bound to the grid and check that instead…oh well). If the user is found in the grid’s store, I know the application state is valid and call:

action.resume();

If the record is not found, I know I have an invalid application state and call:

action.stop();

User Detail: Main Handler

Now that we’ve verified the application state, we can proceed with our main route handler:

/**
 * Handler for matched 'users/:id' route
 * @private
 * @param {Number} id
 */
onUserDetail: function( id ) {
    this.getUserGrid().fireEvent( 'selectuser', id );
}

In this method, I decided that I want to defer most of the processing to the grid’s ViewController, so instead of finishing up all of the needed wiring in this method, I simply created a custom event on the grid itself, fire the event, and pass the matched route userID as an argument. The ViewController will pick up at this point and manage all the bits that we talked about before:

/**
 * Handles selectuser event of grid
 * @private
 * @param {Number} id
 */
 onSelectUser: function( id ) {
    var grid = this.getView().down( 'grid' ),
        store = grid.getStore(),
        record = store.findRecord( 'id', id );
    // if we have the record
    if( record ) {
        grid.getSelectionModel().select( record );
        this.getView().down( '#detail' ).update( record.getData() );
    }
 }

Nothing surprising here. In onSelectUser(), I simply locate the correct user record based on the passed ID, select the appropriate record in the grid (which I already know is there), and then update the detail view.

What Did We Accomplish?

If this is the first time deailing with this, it might seem like we just did a lot of code to create a very roundabout way of accomplishing what is otherwise a simple task. And on the surface, this would appear to be true. However, what we’ve ultimately accomplished is that our application now truly has state for each of these interactions that we’ve defined. No matter how we interact with our grid, we can easily restore any particular state simply by knowing the pattern of the route that we need to match.

But think of this as well. We are not only able to restore state reactively to user interactions (e.g., forward, back buttons). Even more cool, we can proactively set state. Imagine that we have a background process on the server that generates new user records based on some process. Now imagine that we send out an email to the system administrator every time one of these users is created. Because we’ve done the work to implement state within our application, we can now include a link in that email that will allow the system administrator to immediately access this record in our web application without the need for loading the app, navigating to the correct section, and searching for the particular user. And that’s just a simple scenario: with routing, the coolness of the application is limited only by the depth to which you wish to maintain application state.

An Aside: Routes in Controllers vs. Routes in ViewControllers

One frustrating part about the Ext JS 5 Routing Guide is that its implementation is way too simplistic. In the guide, all of the routing is defined in the main ViewController. While this is fine for the purposes of their overly-simplistic example, it doesn’t do much good for a real application for one very simple reason: ViewControllers are created and destroyed with the views to which they are bound. So if we tried to mimic the pattern by placing the routes for our “User” section in the User(View)Controller, we’d never see our actions fire since our User(View)Controller will not have been instantiated yet. Because of this, I created a regular Ext.app.Controller and designated it to manage all of the routing for the users section of this application.

I’ve been looking for a good answer this conundrum, but have yet to find anything satisfying. I would prefer to move away from generic, “always live” controllers and rather adopt ViewControllers for as much as I can, but I really need routing. I could, of course, have one Routing controller that’s responsible for handling all routes. However, this could get majorly out of hand as I’d have to not only manage hundreds of routes within one file, but also all of the refs for the various views that I’d need to interact with on some level within that controller.

What I think would be really great would be a layer that bridges the gap between the ViewController and routing, something that would be able to do the minimal amount required to handle the route but then allow the ViewController to take over and do what it’s good at. I honestly don’t have a great idea for what this looks like…the only thing I know is that I am really uncomfortable with the current mix I have. Suggestions are most welcome!

Wrapping Up

In this installment, we’ve looked pretty in-depth at routing in Ext JS 5. While there is certainly a lot more to the story that what was covered here, I hope the discussion about the implications of using routing within your Ext JS 5 app was helpful.

The subject of routing is, of course, fairly new to me as well, so I would love and appreciate any suggestions, feedback, or constructive criticism of what was covered here.

Thanks!