Have you ever used a single-page application where accidentally hitting the backspace key would “shoot you out” of there? Or when you are used to click back/forward on your browser but now you can’t because the app doesn’t support history? Believe me, it’s annoying.
History support allows your users to better navigate on your app. It also unleashes the power of URLs by enabling deep-linking. Transitioning from page to page is as simple as changing the URL.
It might sound like something really hard to implement, but it’s not. It’s very simple and you are only a few steps away from making your app fully history enabled! Let’s dig into it.
Basic History Support
Ext JS introduces history support with the Ext.util.History class. It solves cross-browser issues with the location.hash API and provides a solid foundation for your app. All you need to know can be summarized in 3 steps:
- Call Ext.util.History.init() to initialize it. You can do this on your app.js entry file
- Listen for changes Ext.util.History.on(‘change’, yourFunctionHere)
- Redirect your users to new views using Ext.util.History.add(“your-token-here”);
Now, Ext.util.History is great but there’s a lot of blanks to be filled. Check this example:
// 1. init Ext.History.init(); // 2. Listen for changes Ext.History.on('change', function(token) { var match; //How to parse and dispatch an action? //Nested if/else logic? if (token === 'users/') renderUserList(); } else if (match = token.match(/users\/(\d)/)) { renderUserForm(match[1]); } // not very scalable... }); // 3. Redirect users with .add() method function onTabChange(newTab) { Ext.History.add(newTab.href); }
Once the hash change occurs, what do you do? Do you parse the token using a big switch/case? And if the token contains data, like a record ID or additional parameters, how do you parse then? We need additional functionality to complement this solution.
Advanced Ext JS History support
After using Ext.util.History for a while I figured it would not be able to solve my application history problem in the best way. It does provides a good foundation, but only when combined with a powerful routing system and fully integrated with Ext MVC is when we reach its true potential. Based on this I created Ext.ux.Router.
routes: { '/': 'home#index', 'users': 'users#list', 'users/:id/edit': 'users#edit' }
Being able to decode the hash token is key for taking advantage of history support. Just like server-side frameworks that are able to parse GET requests, Ext.ux.Router can parse your “hash based URL”.
http://app.website.com/#user/3/contacts?sort=name
In additional to decode the URL we need some action to occur when it changes. The final step for the Router will be to match the proper route and dispatch a controller action.
How to implement
First step after downloading the JavaScript is requiring the class:
Ext.application({ requires: [ 'Ext.ux.Router' ], ...
Second step is to define your routes. There’s a couple ways you can define routes, using regex, etc. The more traditional way is like the one below, where you have routes on the left matching controllers#action on the right.
Ext.application({ ... routes: { '/' : 'home#index', 'settings' : 'settings#index', 'users' : 'users#list', 'users/:id/edit': 'users#edit' }, ...
The last step is simply creating the controller actions:
Ext.define('SinglePage.controller.Users', { extend: 'Ext.app.Controller', edit: function(params) { var userId = parseInt(params.id, 10), usersEdit = this.render('usersedit') //TODO, load user }, ...
With this routing engine sitting on top of Ext.util.History, we can develop a scalable solution for app navigation. When the URL hash changes, the routing system will figure out how to decode it, and all you need to do is develop a controller action. It will receive all params ready for consumption.
Decoupling Code
Personally what I like the most is the fact that it leads to code decoupling. Very often we something like the example below, where we have strong dependencies between views.
/** * User Form depends on User List. If there wasn't a previous list * interaction, this code will break. We'll also have problems in the * future if we develop more ways to get into the User Form, making * this solution poorly scalable */ onUserFormRender: function(userForm) { var selectedUser = this.getUserList().getSelectionModel().getSelection()[0]; userForm.getForm().load({ url: 'users/' + selectedUser.getId() }); },
Now, let’s rewrite this for history support with Ext.ux.Router. A grid selection means redirecting the URL to “#users/3”. After that, the Router engine will solve the URL and the form action will get activated. The action will then render the form and fetch the data from the server with ID 3.
/** * When a record is selected, redirect the user. Enables * history support. */ onUserListSelect: function(userList, record) { Ext.ux.Router.redirect('users/' + record.getId()); }, /** * Action for route "users/:id". It renders the user form. * It doesn't matter how we got here. Only matters that we * have an user id and we will display it. Very scalable solution. */ actionUsersForm: function(params) { var tabPanel = this.getTabPanel(), usersForm = usersForm.child('usersform'); if (!usersForm) { usersForm = tabPanel.add({ xtype: 'usersform' }); } usersForm.getForm().load({ url: 'users/' + params.id }) },
In this situation, all the form view knows is that a user id will be provided, and this will be everything it needs to render itself properly. Isolation :) If I send you an URL saying “check this record /users/3” you would have direct access to the view, without requiring a previous grid interaction.
Router Events
Inside the Router execution flow there are 3 events you can take advantage of: routemissed, beforedispatch and dispatch.
With routemissed event you can handle “local 404 errors”. Every time a route doesn’t match, the routemissed is triggered. Can be useful to show messages or anything.
The other two events, beforedispatch and dispatch, can be used to customize the router execution flow. You can potentially add validations to beforedispatch and prevent the route matching by returning false. You can also hook up rendering logic to the dispatch method.
/* * Ext.ux.Router provides some events for better controlling * dispatch flow */ Ext.ux.Router.on({ routemissed: function(token) { Ext.Msg.show({ title:'Error 404', msg: 'Route not found: ' + token, buttons: Ext.Msg.OK, icon: Ext.Msg.ERROR }); }, beforedispatch: function(token, match, params) { Ext.log('beforedispatch ' + token); }, /** * For this example I'm using the dispatch event to render the view * based on the token. Each route points to a controller and action. * Here I'm using these 2 information to get the view and render. * Example: * * users/:id/edit -> controller Users, action edit -> * renders user.Edit view. */ dispatch: function(token, match, params, controller) { var view, viewClass, action, viewport = Ext.getCmp('viewport'), target = viewport.down('#viewport-target'), navToolbar = viewport.down('#main-nav-toolbar'); // adjust controller and action names action = Ext.String.capitalize(match.action); controller = match.controller.charAt(0).toLowerCase() + match.controller.substr(1); // try to get the view by controller + action names viewClass = Ext.ClassManager.get('SinglePage.view.' + controller + '.' + action); if (viewClass) { // create view view = Ext.create(viewClass, { border: false }); // clear target and add new view target.removeAll(); target.add(view); // adjust top toolbar if (navToolbar.child('#' + controller)) { navToolbar.child('#' + controller).toggle(true); } } } });
Try it!
Ext.ux.Router is open-source an available at Github. The project has about 2 years now and it’s on a pretty solid state. I’ve also just updated the /examples folder so you have more code to start with.
Enabling history support on your app is a huge thing for users and as you saw it’s also very simple to implement. I hope you have all the information for getting the ball rolling, but you can always reach me in case of questions. Enjoy!
Nice on, Bruno!
You need some “share” buttons on this new theme, i.e. FB or G+
Good point! Thanks ;)
@bt_bruno Looks great! controller:action targeting is brilliant.
The next version of Ext JS will have routes baked into controllers.
Yes! And it’s great to have it on the core.
Personally I dislike having routes in the controllers, like Touch has for a while now. Prefer the unified syntax on app.js much better. Nothing some overrides couldn’t customize :)
Thanks for the 2 cents!
Hi Bruno,
Thanks for the post. I have a question though, what if we have a screen where user can search with many parameters, and then from list view user can navigate by double clicking on any record to view details in next view. Now to use back button I need to maintain all the parameters he has used in search, so what is the best way to maintain required request parameters in such condition? Since they could be many forming URL (or route) could be very long.
Thanks for any pointers.
Parag
Nice work bruno??? I am now facing a vulnerability issue with ext.js 4.2 page. When a user has logged in and viewing his profile, in any instance he presses back button of the browser. I need to kill his session. How can I do that with ext.js
[…] URLs to your application flow, and I even developed an extension Ext.ux.Router for Ext 4 and wrote a guides on […]
If I were a Teenage Mutant Ninja Turtle, now I’d say “Kbgaounwa, dude!”