the singularity of being and nothingness
ExtJS 4: A Modified Ext.util.History
I recently implemented Ext.util.History for the first time in a project that I’m working on. If you’re not familiar with History, it allows you to “register arbitrary tokens that signify application history state on navigation actions”. You can then take these “tokens” and use them in your application to display certain views, fire actions, etc. when the user uses their browser’s “back” and “forward” buttons. In other words, it can help you prevent your 1-page AJAX application from reloading completely when someone accidentally hits the back button. But on a more positive note, it allows your AJAX application to use regular navigation conventions of the browser to control in-application functionality. Pretty cool.
The best part about History, however, is that it is stupid-simple to implement. There are basically 3 steps:
- Initialize Ext.util.History
- Build in the “token-building” logic into your application at appropriate places
- Define the listener for handling History events.
That’s it. History is not only easy to implement, but it is also pretty unobtrusive–you can implement it fairly simply into an existing app without completely reworking the whole thing.
There’s a lot more to History, of course, so be sure to check out the docs and Sencha’s example.
First, My Example…
Before I talk about the limitation I found in History–and how I overcame it–let me lay out the flow in my application.
In my app, I have a navigation tree in which each item functions a link to retrieve the “main page” data from an external data source. So with each click in the navigation, there is one AJAX request to get data, and the data is then added to the body of a panel in my app.
The idea for History, then, is that I want to tokenize the necessary parameters for my AJAX call, so that when users go “back” or “forward” in the browser, the AJAX request will be fired for the “previous” or “next” page, and they’ll get the appropriate content returned…just as if they had clicked the link in the navigation tree.
So let’s walk through how I set this up:
First, I initialize Ext.util.History:
Ext.require(['Ext.util.History']); Ext.util.History.init();
Pretty hard, right? 🙂
Next, I’ll set up my History “token” in the appropriate place. In this example, I’m placing it the handler for a custom event called “afterupdate”–this event fires after I receive my data back from the AJAX request, as well as after the content of my main body panel has been updated. I put the History tokenization here because if this event fires, I know that I have valid data to tokenize…
this.control({ "maincontent": {     afterupdate: function(target,type) {       ...       // add to real Ext History       var oldtoken = Ext.util.History.getToken();       var newtoken = type + ":" + target;       if(oldtoken === null || oldtoken.search(newtoken) === -1) {         Ext.History.add(newtoken);       }     }   } })
So this is pretty self-explanatory. We first get the old token (retrieved by calling Ext.util.History.getToken()), as well as build our new token. As you can see, the value is completely up to you, so you can be as specific as you want (just be sure to use valid URL characters…).
Finally, if the old token is not defined or empty string, OR not equal to the new token (e.g., we clicked the same link twice in a row), we execute History’s add() method.
The last part of this is the listener for the “change” event. It’s here that everything gets pulled together:
Ext.util.History.on('change', function(token) { Â Â if (token) { Â Â Â Â var page = token.split(":"); Â Â Â Â me.getController("Navigation").loadfromhistory(page[1],page[0]); Â Â } });
If a token exists, we split it on the same delimiter we used when creating the token. With these values, we can then do whatever we want–such as executing the loadfromhistory() method which will trigger an AJAX request for the specified page.
…Then the Limitation
While this works brilliantly, there is a small limitation, and it’s simply that the “change” event is fired when the browser back/forward buttons are clicked, OR when the add() method is executed. In the context of my application that basically means the following occurs:
Browser Back/Forward Click:
- loadfromhistory() method executed (good)
add() Method:
- Original AJAX request executed
- Main panel updated
- add() method fires –> triggers History change event
- loadfromhistory() method executed (bad)
As you can see, when adding a token, I basically get the same AJAX request executed twice. While this doesn’t hurt anything really (well, unless I didn’t build my token right…), it’s a waste; there’s no need to make two requests for one set of data.
Unfortunately, out of the box I couldn’t figure out a way to get around this. There’s no “suppressChange” flag for the add method, and there’s no additional information that I can key in on within the change event itself to different between these two scenarios.
What’s more, it doesn’t *appear* to be bad implementation on my part (I’ll take correction on that if I’m off base on this). If you look at the example for Ext.util.History, the same behavior occurs: the setActiveTab() method is executed twice when the History add() method is invoked (e.g., when a tab is clicked), but only once when the browser buttons are used.
The Workaround
After doing a bit of Googling, and finding some common complaints about the lack of event suppression on the add() method of Ext.util.History, I decided to implement a “fix” via Ext.apply(). Here’s the final result:
Ext.apply(Ext.util.History,{ suppressChange : false, add: function(token,preventDup,suppressChange) { var me = this; this.suppressChange = suppressChange ? true : false; if (preventDup !== false) { if (me.getToken() === token) { return true; } } if (me.oldIEMode) { return me.updateIFrame(token); } else { window.top.location.hash = token; return true; } }, handleStateChange: function(token) { this.currentToken = token; // only fire "change" event if suppressChange is false if (!this.suppressChange) { this.fireEvent('change', token); } // now just reset the suppressChange flag this.suppressChange = false; } });
There are really 3 parts that are important.
The first is that I added another property to Ext.util.History, namely “suppressChange.” This flag will tell History whether or not to fire the change event.
The second is that I added another argument to the add() method, again, “suppressChange”. It will simply set the value of the Ext.util.History.suppressChange property.
Finally, in handleStateChange(), I wrapped the change event firing in a condition: if Ext.util.History.suppressChange is false, the change event will be fired; otherwise, it will not. And to wrap it up, the handleStateChange() method automatically resets the default value of the suppressChange property back to false so I don’t have to bother with that elsewhere, and since not suppressing the change event is the desired default behavior.
What this boils down to, then is that you don’t have to muck with your change event listener at all. Rather, you can simply add the suppressChange flag when executing the Ext.util.History.add() method, and let the rest flow on as normal:
// add() invocation with suppressChange flag set to true Ext.History.add(newtoken,true,true); //token, preventDuplicates, suppressChange
Wrapping Up
And that’s that. To be perfectly honest, I’m not sure if this is *best* way to go about this, but in my testing it does work, at least for my purposes. I’d love to hear *constructive* feedback, suggestions on how to do it better, etc.
Print article | This entry was posted by existdissolve on August 21, 2011 at 2:23 pm, and is filed under ExtJS. Follow any responses to this post through RSS 2.0. You can leave a response or trackback from your own site. |
about 12 years ago
I’ve been wrestling with the same thing and decided on basically the same solution. I had a couple of other ideas, but they all seemed incredibly dirty or overly complicated (such as making all my history-enabled actions fire through the history change event handler, using it as a sort of event dispatcher).
It seems pretty intuitive to want to add history states without triggering change events, after all, it strikes me that the usual course of action is to tell the history an action has/will be performed and things changed so that when the back button triggers, something can be done about it. To want to handle a change in the history log when an action is taking place seems a rare occurrence to me. Perhaps add shouldn’t fire a ‘change’ event by default, but rather an ‘added’ one?
about 12 years ago
Thanks for the link. Regarding your isseus with serialization, you will have a better luck with Newtonsoft library. It serialize better and Ext JS seems to like it better
about 12 years ago
Hi Andrew–
I definitely agree with you. I think “adding” history and “recognizing” history should be broken out.
It’s funny, because even the example of History on Sencha’s site does what amounts to a double-dip of firing the setActiveTab() method. Obviously, you don’t really notice it as it’s only setting a view (of static content), but it *does* happen. I think this is a little inefficient, but more importantly makes History–which is otherwise ridiculously cool–somewhat clunky to work with, especially if your history actions are more robust than setting a tab in a panel.
I also like your idea of the “added” event for add actions. That would help make the segregation of history “actions” much more clear.
Thanks for your feedback!
about 12 years ago
What Sencha did in their 4.0 pre-release code was to call History.suspendEvents() before adding a token, and then call .resumeEvents (with a short delay).
History.suspendEvents(false);
History.add(options.historyUrl);
Ext.defer(History.resumeEvents, 100, History);
Not sure why they didn’t go with a ‘suppress’ argument on the add() method like you did – seems a lot cleaner to me.
I’ve heard 4.1 might re-introduce the Router and Dispatcher class. Let’s see with what solution they came up with.
about 12 years ago
Thanks for the article. The double firing is a problem and I hope Sencha adds the suppress argument like you did.
@mistarecko, thanks for the suspendEvent tip. That actually worked pretty well.
The router and dispatcher still did not make it into 4.1, so I ended up writing an article on how to achieve this using PathJS: http://blog.falafel.com/blogs/12-02-17/Building_a_Javascript_MVC_App_using_Sencha_Ext_JS.aspx
about 10 years ago
and what about this trick without the proposed fix?
Ext.History.suspendEvents();
Ext.History.add(…);
Ext.History.resumeEvents();
about 9 years ago
This is wonderful exactly what I am looking for !!
But I am pretty much new to extjs, I would like to know, where should I add this Ext.apply () code in my project, is it in the controllers init() or Application.js file launch() method.
I am using Sencha Architect for my development
about 9 years ago
You should be able to do it in launch, before you initialize history.
about 9 years ago
Hi Waston,
I did it in init() of Appliation, and it seems to be working. But I am facing more specific problem wiht my project, managing the history.
I have viewport with cardlayout with two items.
The first item is a login screen and second item is main screen.
The main screen is border layout with west region containing a “menu” and center with a cardlayout for corresponding menu.
I am doing “Ext.Hisotry.add(token)” in the activate event of the corresponding card layouts.
I tried debugging in the chrome. and I see that the tokens are being added and immediately a empty token is being added out of no where.
so when click on the menu, the url should change to localhost/myapp.html#App:main:menu1.
But what is happening is some how because of the empty otken being added, it is qickly changing back to localhost/myApp.html#,
And I have to click on browser backward/forward button twice to navigate thru the history.
Any pointers about what I could be missing would be greatly helpful
about 9 years ago
Hi Uday–
It sounds like you’re calling add() too many times. Maybe instead of keying on the activate event, you should target the actions that are triggering the card layout change. This would also help your debugging immensely since you’d have a bit more visibility into when, exactly, you’re code is getting executed.
about 9 years ago
I have usecase, where not just by clicking menu on the left, but clicking some links inside menu1 should take to menu2 and vice versa.. That’s reason for doing in the activat event of inidividual item in the card lay out directly
I added some alerts and console log statements. Based on them, it looks as if the activate is called only once, but some how an empty token is being added immediately after the actual token is added.
about 9 years ago
Perhaps you should set suppressChange *after* checking for duplicates otherwise this can happen
—> task: hash = x
add(‘x’, true, true) // dupe but still sets this.suppressChange to true
–> task: hash = x // same as before so leaves this.suppressChange set to true
change hash (e.g. by typing in address bar)
–> task: hash = y // oops! change suppressed
Alternatively, you could reset suppressChange in the startUp task but this requires overriding part of Ext.History before init is called.
Ultimately, Ext.History is a leaky abstraction. Anyone using it should look under the covers to avoid being surprised.