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:

  1. initialize History object with loadPage method of PageFlow object
  2. change default behavior of the links - they must invocate load method of History object 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 });
blog comments powered by Disqus