Table of Contents
Introduction
Ext JS 5 is a major release that offers many new features to create rich, enterprise grade web applications. MVVM and two-way data binding do a lot of the heavy lifting for developers. Another new feature in Ext JS 5 is Routing, which makes history support easy to manage within a controller. The forward and back buttons are common parts of the user interface on every browser – and handling this navigation within a single page application is now very simple with Ext JS 5.
Ext JS 5 Routing
Ext JS has always allowed you to handle history changes using the Ext.util.History class, but in Ext JS 5 we made the process much easier and more flexible. The router provides an easy configuration to map hash tokens to controller methods, with parameters and before actions to control the flow of route execution, and uses Ext.util.History behind the scenes. Let’s look at a simple example:
Ext.define('MyApp.controller.Main', { extend : 'Ext.app.Controller', routes : { 'home' : 'onHome' }, onHome : function() {} });
In the routes object, the key (‘home‘) is the hash to match, and the value (‘onHome‘) is the method on the controller to execute when the hash is matched (for example: http://localhost#home). To change a hash within a controller, you can use the redirectTo method:
this.redirectTo(‘home’); //redirects to http://localhost#home
This will change the URL hash to #home which will then execute the onHome method scoped to the MyApp.controller.Main controller instance that configured the route. If you have multiple controllers that match the same hash token, the order of execution will be the order defined in the Application instance in the controllers array.
Hash Tokens and Parameters
A hash token can also contain parameters, and the router makes it simple to handle by passing them to the controller method as arguments. A hash with parameters may look like ‘#user/1234’ where 1234 is the user ID and should be treated as a parameter. A controller can be configured to listen to the hash in this way:
Ext.define(‘MyApp.controller.Main', { extend : 'Ext.app.Controller', routes : { 'user/:id' : 'onUser' }, onUser : function(id) {} });
When you configure a route to expect a parameter, you should use a colon followed by the name of the parameter, in this case :id is the parameter. The router will match any value passed as the parameter and then will pass this parameter to the onUser method. The order of arguments passed to the controller method is the same order that is defined in the configured route.
You can also control the matching of hash parameters based on a regular expression. In the user ID example, the ID can only be digits and any other value should not be matched. In order to control this matching, the route needs to use the conditions config:
Ext.define('Fiddle.controller.Main', { extend : 'Ext.app.Controller', routes : { 'user/:id' : { action : 'onUser', conditions : { ':id' : '([0-9]+)' } } }, onUser : function(id) {} });
This example introduces two things: the route can be an object where the action key is the controller method and the conditions config is used. The conditions config is an object of parameters and regular expression strings. The reason it’s a regular expression string and not an actual regular expression is the router creates a master regular expression based on the parameters within the route and the conditions config allows you to override the default matching regular expression string that is used. The default regular expression string for parameters is ‘([%a-zA-Z0-9-_s,]+)’.
If no route is configured to match a hash that occurs, an unmatchedroute event will be fired on the application. This event can be listened for on the application or a controller, each in the same way. Here is an example of listening in a controller:
Ext.define('Fiddle.controller.Main', { extend : 'Ext.app.Controller', listen : { controller : { '*' : { unmatchedroute : 'onUnmatchedRoute' } } }, onUnmatchedRoute : function(hash) {} });
There may be times when you need to hook into the route execution process to prevent a route from continuing to execute or delay the execution for some asynchronous action like an ajax request. In order to do this, a route can be configured with a before action and also passed any parameters configured in the route. Here is an example of using an ajax request and continuing the route after the request has finished:
Ext.define('Fiddle.controller.Main', { extend : 'Ext.app.Controller', routes : { 'user/:id' : { action : 'onUser', before : 'beforeUser', conditions : { ':id' : '([0-9]+)' } } }, beforeUser : function(id, action) { Ext.Ajax.request({ url : '/user/confirm', params : { userid : id }, success : function() { action.resume(); }, failure : function() { action.stop(); } }); }, onUser : function(id) {} });
The beforeUser method receives the id parameter like in the onUser method, but it also gets an action argument. The action argument has a resume and stop method that controls the route’s execution. Executing action.resume(); like in the success handler of the Ext.Ajax.request will resume the route’s execution; this is what allows the route to be asynchronous. Executing the action.stop(); method, as seen in the failure callback, will stop the current route from executing. If true is passed to the stop method, all queued routes will stop executing, allowing you to have complete control over the routes.
Ext JS applications can become large and complex, and they may require multiple hash tokens to be active at the same time. Ext JS 5 has the ability to handle multiple hash tokens and execute them separately from each other; the separate tokens will be sandboxed. This means that if you cancel one route by passing true to the action.stop method, it will only prevent the other routes for that hash token; the other hash tokens will continue to execute. Each token needs to be pipe delimited. An example hash would look like this:
#user/1234|message/5ga
The router will split this hash and have the ‘user/1234‘ and ‘message/5ga‘ tokens. The router will start with the user token and find all routes that match that token and execute any matched routes. If no routes match the token, the unmatchedroute event will be fired. The router will then move onto the message token and find all routes and execute them. If no routes match the token, the unmatchedroute event will be fired.
Conclusion
The new router in Ext JS 5 allows you to handle the browser history stack as simply as a configuration, yet it’s still flexible and powerful to meet complex application needs. Together with the MVC+VM, two-way data binding and the other new features, Ext JS 5 is the perfect framework for enterprise grade applications.
Would be great to add link to Sencha Fiddle (not for me but for everyone else who is looking for working example). Anyway great post for everyone who needs routes in extjs app :)
@Zdeno, thank you for your comment. The issue with having a Sencha Fiddle example for routing is that Sencha Fiddle doesn’t expose the uri of the actual fiddle which is crucial to understanding everything in the big picture. You would only be able to view the code and the code would work but you wouldn’t be able to see what is actually going on and what to expect.
This routing model looks similar to sencha touch. It’s been really nice. The only issue I’ve run into is when you need to return to a previous url if a form is dirty and you need to do something before you leave.
@ernest leitch, We designed the router to be very API compliant with Touch, the added features only add onto the API. If you need to do something before you leave a component you can do it one of two ways: 1) Use a before action and only execute action.resume() when that something is done, action.resume supports async. 2) Do the something before you use the redirect method on the controller so the hash doesn’t update until you tell it to.
Of course, this will all depend on the way you architect your application. I find it best to think about using routes at the beginning and throughout developing your application. Doing it at the end can cause a lot of headaches while you refactor code where as you can architect your code to work with routes from the beginning.
@mitchell I make heavy use of the before action. I can stop the view from changing by not using action.resume(), but the uri will still change. I had to use window.history.forward() to return to the previous uri.
I had a forum thread about it: https://staging.sencha.com/forum/showthread.php?281844-Prevent-URI-change-through-browser-back-button
That said the router class in touch is really slick. It’s been great to work with.
@ernest leitch, unfortunately the browser will not allow you to prevent a hash from changing, you can only react to it.
Looking forward to see how this is integrated into the upcoming Architect update. Looks very useful.
@Alexey Solonets, Yes, that does look like a typo. The action.stop method was a change after public beta was released but after this blog was actually written and looks like that spot was missed.
@Mitchell thanks, it seems to be working and it is what I was looking for.
@Mitchell Simoens
>>This means that if you cancel one route by passing true to the action.resume method …
Is it a typo? Did you mean action.stop method?
Looks useful.
Certainly looks like worth including from the start of an application development…
Also, can multiple params be passed to a route? E.g. ‘user/:id,:name’ or something?
@Westy, Yes, you can have any number of params. Say you setup a route like this:
‘user/:id/:name’ : ‘onUser’
This will match a hash like ‘user/123/Bob’ and the onUser method would get both arguments:
onUser : function(id, name) {}
Can you chain routes ?
For example let’s assume you have a MainController with route ‘/users’ which loads UsersView into the center panel and a UsersController for the UsersView with a route ‘users/:id’ which loads edit screen for a user.
So when accessing the url /users/123 the route would be resolved first by the first controller (MainController) route (‘users’) which loads the UsersView with UsersController which resolves the second part of the route (‘/:id’). Does extjs support this kind of behaviour ?
I’m asking this because in big applications with hundreds of routes there is the need have routes defined in multiple controllers which are not active all the time (like main controller) and also it is ugly to have a hundred routes in a single controller (like main controller).
How can one achieve that ?
Thanks
@Marius, You have define multiple hash tokens within a single hash:
#user/1234|messages
Which will split the string by the pipe and run each hash sandboxed from the other. So ‘users/1234’ will execute and then ‘messages’ will execute.
To supplement this excellent Mitchell’s post, I’ve written the example that implements the approach described here. The implementation uses a global MVC controller that is one of the places where to setup routing. I’ll write another example that will implement the routing using MVVM view controller in a couple of days.
http://extjs.eu/ext-examples/#route-mvc
Enjoy!
Saki
Here is the promised MVVM implementation. It is in a mixin used by the main view controller, however, the mixin is not mandatory. I have used it only to separate the routing logic from the rest of controller. It is also easy to disable routing (e.g. for debugging) just by commenting the mixin line in the controller.
http://extjs.eu/ext-examples/#route-mvvm
Hi thanks for this article. We’ve translated it for Japanese users here:
http://www.xenophy.com/sencha-blog/11285
Also for any Tokyo based Sencha users, this is the meetup group:
http://www.meetup.com/Japan-Sencha-User-Group/
By the way Russian translation is also available here
http://habrahabr.ru/post/226759/
Am I the only one to whom the paragraphs are cutted? What happens with this non-breaking spaces?
Oh, it seems an issue with the pre tags.
Do not nest p tags inside pre tags, please :S
Thanks. The formatting has been corrected.
Very nice post and right to the point. I am not sure if this is in fact the best place to ask but do you
guys have any thoughts on where to get some professional writers?
Thanks in advance :)