Back-button problem
There are plast of different problems when you creating ajax-based RIA. Most popular is, in my opinion, so called ‘back-button problem’.
Here I share my solution of this problem, hope it will help you.
The idea is very simple - all dynamic content on your page load by special function (I called that method loadPage at PageFlow object). This method accept hash as param, so that if user clicks on the link which href is anchor, and someone else invocate this method with that anchor as param, your this method will load appropriate data. Of course, we need cover case when user just came from the another page to the root of our application so that there is no one hash param and load ‘default’ data.
Assume, you use jQuery as javascript framework and you develop usual e-commerce site which has whole list of goods and details view of goods item.
The main object, called History, store callback function and can store history of location.hash changes. When hash changes (user clicks on the ‘forward’ or ‘backward’ button or on the links on your page) History Object will invocate callback function with hash as param:
1 /**
2 * History managment, for ajax-based pages
3 * @class History
4 * @constructor
5 */
6 History = function () {
7 var
8 /**
9 * @property currentHash
10 * @private
11 */
12 currentHash,
13 /**
14 * @property _callback
15 * @private
16 */
17 _callback,
18
19 historyBackStack,
20
21 historyForwardStack,
22
23 isFirst,
24
25 dontCheck,
26
27 check = function () {
28 var i, hash;
29 if($.browser.msie) {
30 // On IE, check for location.hash of iframe
31 var ihistory = $("#APHistory")[0];
32 var iframe = ihistory.contentDocument || ihistory.contentWindow.document;
33 hash = iframe.location.hash;
34 if(hash != currentHash) {
35
36 location.hash = hash;
37 currentHash = hash;
38 _callback(hash.replace(/^#/, ''));
39
40 }
41 } else if ($.browser.safari) {
42 if (dontCheck) {
43 var historyDelta = history.length - historyBackStack.length;
44
45 if (historyDelta) { // back or forward button has been pushed
46 isFirst = false;
47 if (historyDelta < i =" 0;" i =" 0;" cachedhash =" historyBackStack[historyBackStack.length" currenthash =" location.hash;">= 0) {
48 _callback(document.URL.split('#')[1]);
49 } else {
50 _callback('');
51 }
52 isFirst = true;
53 }
54 }
55 } else {
56 // otherwise, check for location.hash
57 hash = location.hash;
58 if(hash != currentHash) {
59 currentHash = hash;
60 _callback(hash.replace(/^#/, ''));
61 }
62 }
63 };
64
65 return {
66 initialize : function (callback) {
67 _callback = callback;
68 currentHash = location.hash;
69
70 if ($.browser.msie) {
71 // To stop the callback firing twice during initilization if no hash present
72 if (currentHash == '') {
73 currentHash = '#';
74 }
75
76 // add hidden iframe for IE
77 $("body").prepend('<iframe id="APHistory" style="display: none;"></iframe>');
78 var iframe = $("#APHistory")[0].contentWindow.document;
79 iframe.open();
80 iframe.close();
81 iframe.location.hash = currentHash;
82 } else if ($.browser.safari) {
83 // etablish back/forward stacks
84
85 historyBackStack = [];
86 historyBackStack.length = history.length;
87 historyForwardStack = [];
88 isFirst = true;
89 dontCheck = false;
90 }
91 _callback(currentHash.replace(/^#/, ''));
92 setInterval(check, 100);
93 },
94
95 add : function (hash) {
96 // This makes the looping function do something
97 historyBackStack.push(hash);
98
99 historyForwardStack.length = 0; // clear forwardStack (true click occured)
100 isFirst = true;
101 },
102
103 /**
104 *
105 * @param hash {String} desiring hash without first #
106 */
107 load: function(hash) {
108 var newhash;
109
110 if ($.browser.safari) {
111 newhash = hash;
112 } else {
113 newhash = '#' + hash;
114 location.hash = newhash;
115 }
116 currentHash = newhash;
117
118 if ($.browser.msie) {
119 var ihistory = $("#APHistory")[0]; // TODO: need contentDocument?
120 var iframe = ihistory.contentWindow.document;
121 iframe.open();
122 iframe.close();
123 iframe.location.hash = newhash;
124 _callback(hash);
125 }
126 else if ($.browser.safari) {
127 dontCheck = true;
128 // Manually keep track of the history values for Safari
129 this.add(hash);
130
131 // Wait a while before allowing checking so that Safari has time to update the "history" object
132 // correctly (otherwise the check loop would detect a false change in hash).
133 var fn = function() {AP.History.setCheck(false);};
134
135 window.setTimeout(fn, 200);
136
137 _callback(hash);
138 // N.B. "location.hash=" must be the last line of code for Safari as execution stops afterwards.
139 // By explicitly using the "location.hash" command (instead of using a variable set to "location.hash") the
140 // URL in the browser and the "history" object are both updated correctly.
141 location.hash = newhash;
142 }
143 else {
144 _callback(hash);
145 }
146 },
147
148 /**
149 * Set need we check, or not.
150 * @param check {Boolean}
151 * @protected
152 */
153 setCheck : function (check) {
154 dontCheck = check;
155 },
156
157 /**
158 * @method getCurrentHash
159 * @return {String}
160 */
161 getCurrentHash : function () {
162 return currentHash;
163 }
164 };
165 }();
PageFlow object, that I describe above looks like this:
1 /**
2 * Page Flow controller - load hash-specific data, show appropriate container and all that
3 * @Class PageFlow
4 */
5 var PageFlow = function () {
6 /**
7 * show whole list of goods
8 * @method loadListOfGoods
9 * @private
10 */
11 loadListOfGoods = function () {
12 $('#goodsItemDetails').css('display', 'none');
13 $('#listOfGoodsWorkArea').css('display', 'block');
14 },
15 /**
16 * show detailed goods item view
17 * @method loadGoodsItemDetails
18 * @private
19 */
20 loadGoodsItemDetails = function (id) {
21 var item = M.ListOfGoods.getGoodsItemById(id);
22 if (item.pluralizedProfit.length == 0) {
23 item.pluralizedProfit = item.pluralizedPrice;
24 }
25 // fill container with appropriate data
26 M.Renderer.renderGoodsItemDetails([item]);
27 // show goodsItemDetails
28 $('#goodsItemDetails').css('display', 'block');
29 $('#listOfGoodsWorkArea').css('display', 'none');
30 },
31
32 return {
33 /**
34 * decide what page to load
35 * @method loadPage
36 * @param pageName {String|Number} location.hash with stripped `#` sign
37 * @public
38 */
39 loadPage : function (pageName) {
40 if (L.isUndefined(pageName)) {
41 if (M.ClientURI.isMainPage()) {
42 loadListOfGoods();
43 }
44 }
45 // if pageName is number
46 if (L.isNumber(pageName) || pageName.replace(/\d+/, '').length == 0) {
47 // need to load goods item details page with provided id
48 loadGoodsItemDetails(pageName);
49 } else {
50 switch (pageName) {
51 case 'all':
52 loadListOfGoods();
53 break;
54 }
55 }
56 }
57 };
58 }();
That is almost the end, only two things left:
- initialize
Historyobject withloadPagemethod ofPageFlowobject - change default behavior of the links - they must invocate
loadmethod ofHistoryobject instead of send user to anchor location area
1 $(function () {
2 $('a @rel=[history]').click(function () {
3 History.load(this.href.replace(/^.*#/, ''));
4 return false;
5 });
6
7 History.initialize(PageFlow.loadPage);
8 });