the singularity of being and nothingness
Ext JS 5: Routing
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
- Source files can be found/forked here: https://github.com/existdissolve/ExtJS5Explorer
- Simple demo can be found here: http://existdissolve.com/ExtJS5Explorer/routes/#users
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!
Print article | This entry was posted by existdissolve on August 10, 2014 at 1:28 pm, and is filed under Ext JS 5. Follow any responses to this post through RSS 2.0. You can leave a response or trackback from your own site. |
about 10 years ago
Very happy that a feature I implemented being blogged about. 🙂 It’s not a flashy feature but one that any dev should appreciate.
One thing I’d like to point out is for the #users/3 hash as described doesn’t need to load the full users grid first, fully dependent on application structure. An application should handle the use case of loading just the user detail. In the example in this blog since the grid is show above the user detail it makes sense but not all application usages will match so just want to make sure people don’t think you need to instantiate every view that would lead up to the hash that is being loaded.
Also, for the ViewController, your MainController is a sort of global controller since you are not recreating a viewport, Main view is a viewport so the routes could be handled in MainController since it will not get destroyed. This means you can use this.getView() to always get the Main view instead of adding a ref. You can then use a reference to the users grid also instead of using a ref.
about 10 years ago
It might not be “flashy” in the sense of visuals, but I think it’s one of the more important features that’s been added to Ext JS since MVC. A baked-in solution for routing will hopefully unify the dozens of home-brewed versions that have been out there in its absence. Thanks!
Good point on the view dependency note. I will add a note to the blog post to clarify 🙂
Re: the routes in the MainController, I am still a bit unsatisfied with that solution. That’s what I originally did, but then I thought through porting my existing large application at work to Ext JS 5, and the thought of hosting all the routing plumbing in one file (hundreds of routes) seemed completely unmaintainable. While I also don’t relish the idea of having dozens of controllers *just* for the sake of modularizing routes, it seems less of a maintenance nightmare than one giant file. Or maybe not. I dunno, I guess I’m just not satisfied with either approach 🙁
about 10 years ago
Hi there,
I was wondering when you would start writing your tutorial about extjs 5.0.*. It’s great and Sencha did a great job!
Great tutorial so far. Maybe you could consider adding some small extra features like optional parameters, and validation with regex. Also keep in mind that you must call the resume or stop from the action in de before statement. From the docs:
You MUST execute the resume or stop method on the action argument. Executing action.resume(); will continue the action, action.stop(); will not allow the action to resume but will allow other queued actions to resume, action.stop(true); will not allow the action and any other queued actions to resume.
Routing is nice but can be very complex. I’m working at an existing sencha touch app at the moment where routing is overdone and all placed in the application controller. I think it’s best practice to route per controller and don’t overdo it..
about 10 years ago
Hi Peter–
Thanks for the compliments! I really appreciate it!
Re: the validation, I was originally planning on including that. However, I think I found a bug (duhn duhn duhn), but haven’t had a chance to verify yet. I think I will do a follow-up post once I figure out.
And yes, I agree about the complexity. I also have a fairly involved Touch app that suffers from the same fate :). That’s one of the reasons I’m still struggling a bit internally about the best way to go about implementing routing. In principle, the concepts laid out in the documentation and guides seem straightforward enough. However, I don’t think they come enough from the perspective of a large app that is more complex and dynamic than an immediately rendered tab panel :). Even something on scale of route-i-tizing the new Tickets app in the Ext JS 5 examples would be nice.
about 10 years ago
Good post. Thanks!
about 10 years ago
>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.
You can create a ViewController without a view. Just new it or Ext.create it. Once you’ve got all the ViewController instances you need, just route by going something like
YourApp.app.redirectTo(Ext.util.History,getToken(), true);
I find it annoying that routing has to be done through a ViewController. If Sencha had made routing a mixin (Routable) then you could route on any component, e.g. a ViewModel or an object designed to create and manage multiple views. Fortunately, Routable is not difficult to write given that you can create a ViewController.
about 10 years ago
Views shouldn’t know about routes, that’s business logic which should go in a controller.
If you don’t have a global view (like a viewport) that can have a ViewController, you can use a global controller on the Application instance.
about 9 years ago
I agree that views shouldn’t know about routes.
But why should I have to extend a particular class and have an instance of that class just for routing?
Routing is a service. Subscription to that service shouldn’t be forced through a particular “base” class like ViewController.
Why not do routing through a mixin?
Then you can handle subscription anywhere you want and Sencha could continue to offer routing through Controller and ViewController for MVC fans by mixing Routable into those classes.
about 9 years ago
Funny, mixin the routes is exactly the way I implemented it.
about 10 years ago
Sure, you can do that. But I think that kind of defeats the purpose of what I was hoping to accomplish.
That being said, you don’t have to have a ViewController for routing. You can create a regular controller as well and use it for routing if that’s preferable.
about 7 years ago
Have the routing issues you mention improved with the latest version of ExtJS?