Night thoughts

Posts tagged javascript

Nov 24

Back-button problem

We face tons of problems while creating ajax-based RIA. It seems like the most popular is so called ‘back-button problem’.

I’m going to share my solution of this problem. I hope it will help you.

The idea is very simple. We have the object, whose job is to load required content to the main container. Lets name it PageFlow. PageFlow has a public method loadPage that accepts a pageName hash as a parameter and loads the content according to the pageName.

There is also an object named History, which is initialized immediately after the page load. History determines the anchor part of the current page URL (the part that goes after #) and finds the corresponding pageName hash. Then it calls pageFlow.loadPage with pageName as a parameter. History object also memorizes when user cliks on anchored links.

So the seqeuence will look like this: when user clicks on the link with the item’s id, History object is invoked. History then calls PageFlow.loadPage(), which loads the content to the page. If user clicks on backward or forward button, everything will work as expected.

Now let’s assume we are developing a shop that has a page with a list of all items (anchor ‘all’) and a details page for each item (anchor ‘id’). Our framework of choice is jQuery.


/**
 * History managment, for ajax-based pages
 * @class History
 * @constructor
 */
History = function () {
    var
    /**
     * @property currentHash
     * @private
     */
    currentHash,
    /**
     * @property _callback
     * @private
     */
    _callback,

    historyBackStack,

    historyForwardStack,

    isFirst,

    dontCheck,

    check = function () {
        var i, hash;
        if($.browser.msie) {
            // On IE, check for location.hash of iframe
            var ihistory = $("#APHistory")[0];
            var iframe = ihistory.contentDocument || ihistory.contentWindow.document;
            hash = iframe.location.hash;
            if(hash != currentHash) {

                location.hash = hash;
                currentHash = hash;
                _callback(hash.replace(/^#/, ''));

            }
        } else if ($.browser.safari) {
            if (dontCheck) {
                var historyDelta = history.length - historyBackStack.length;

                if (historyDelta) { // back or forward button has been pushed
                    isFirst = false;
                    if (historyDelta = 0) {
                        _callback(document.URL.split('#')[1]);
                    } else {
                        _callback('');
                    }
                    isFirst = true;
                }
            }
        } else {
            // otherwise, check for location.hash
            hash = location.hash;
            if(hash != currentHash) {
                currentHash = hash;
                _callback(hash.replace(/^#/, ''));
            }
        }
    };

    return {
        initialize : function (callback) {
            _callback = callback;
            currentHash = location.hash;

            if ($.browser.msie) {
                // To stop the callback firing twice during initilization if no hash present
                if (currentHash == '') {
                    currentHash = '#';
                }

                // add hidden iframe for IE
                $("body").prepend('');
                var iframe = $("#APHistory")[0].contentWindow.document;
                iframe.open();
                iframe.close();
                iframe.location.hash = currentHash;
            } else if ($.browser.safari) {
                // etablish back/forward stacks

                historyBackStack = [];
                historyBackStack.length = history.length;
                historyForwardStack = [];
                isFirst = true;
                dontCheck = false;
            }
            _callback(currentHash.replace(/^#/, ''));
            setInterval(check, 100);
        },

        add : function (hash) {
            // This makes the looping function do something
            historyBackStack.push(hash);

            historyForwardStack.length = 0; // clear forwardStack (true click occured)
            isFirst = true;
        },

        /**
         *
         * @param hash {String} desiring hash without first #
         */
        load: function(hash) {
            var newhash;

            if ($.browser.safari) {
                newhash = hash;
            } else {
                newhash = '#' + hash;
                location.hash = newhash;
            }
            currentHash = newhash;

            if ($.browser.msie) {
                var ihistory = $("#APHistory")[0]; // TODO: need contentDocument?
                var iframe = ihistory.contentWindow.document;
                iframe.open();
                iframe.close();
                iframe.location.hash = newhash;
                _callback(hash);
            }
            else if ($.browser.safari) {
                dontCheck = true;
                // Manually keep track of the history values for Safari
                this.add(hash);

                // Wait a while before allowing checking so that Safari has time to update the "history" object
                // correctly (otherwise the check loop would detect a false change in hash).
                var fn = function() {AP.History.setCheck(false);};

                window.setTimeout(fn, 200);

                _callback(hash);
                // N.B. "location.hash=" must be the last line of code for Safari as execution stops afterwards.
                //      By explicitly using the "location.hash" command (instead of using a variable set to "location.hash") the
                //      URL in the browser and the "history" object are both updated correctly.
                location.hash = newhash;
            }
            else {
              _callback(hash);
            }
        },

        /**
         * Set need we check, or not.
         * @param check {Boolean}
         * @protected
         */
        setCheck : function (check) {
            dontCheck = check;
        },

        /**
         * @method getCurrentHash
         * @return {String}
         */
        getCurrentHash : function () {
            return currentHash;
        }
    };
}();

PageFlow object, that I describe above looks like this:


/**
 * Page Flow controller - load hash-specific data, show appropriate container and all that
 * @Class PageFlow
 */
var PageFlow = function () {
    /**
     * show whole list of goods
     * @method loadListOfGoods
     * @private
     */
    loadListOfGoods = function () {
        $('#goodsItemDetails').css('display', 'none');
        $('#listOfGoodsWorkArea').css('display', 'block');
    },
    /**
     * show detailed goods item view
     * @method loadGoodsItemDetails
     * @private
     */
    loadGoodsItemDetails = function (id) {
        var item = M.ListOfGoods.getGoodsItemById(id);
        if (item.pluralizedProfit.length == 0) {
            item.pluralizedProfit = item.pluralizedPrice;
        }
        // fill container with appropriate data
        M.Renderer.renderGoodsItemDetails([item]);
        // show goodsItemDetails
        $('#goodsItemDetails').css('display', 'block');
        $('#listOfGoodsWorkArea').css('display', 'none');
    },

    return {
        /**
         * decide what page to load
         * @method loadPage
         * @param pageName {String|Number} location.hash with stripped `#` sign
         * @public
         */
        loadPage : function (pageName) {
            if (L.isUndefined(pageName)) {
                if (M.ClientURI.isMainPage()) {
                    loadListOfGoods();
                }
            }
            // if pageName is number
            if (L.isNumber(pageName) || pageName.replace(/\d+/, '').length == 0) {
                // need to load goods item details page with provided id
                loadGoodsItemDetails(pageName);
            } else {
                switch (pageName) {
                    case 'all':
                        loadListOfGoods();
                        break;
                }
            }
        }
    };
}();

Things are getting simple now.
History object is initialized on page load, given PageFlow.loadPage as a callback parameter. All links where ‘rel’ attribute is ‘history’ are assigned an event listener that calls History.load istead of default jumping to the anchor.


$(function () {
    $('a @rel=[history]').click(function () {
        History.load(this.href.replace(/^.*#/, ''));
        return false;
    });

    History.initialize(PageFlow.loadPage);
});

Jun 12

Funny comment


// I feel a dark side of Power here.
this.theContainer = jQuery("<div></div>")
  .attr("id", this.theTextarea.id + "Container")
  .addClass("widgContainer")
  .append(
      jQuery("<div></div>")
        .addClass("buttons")
        .append(this.theToolbar.theList)
   )
  .append(this.theTabs.theTabsList)
  .append(this.theIframe)
  .append(this.theInput)
  .append(this.theExtraInput)
  .hide()
  .get(0);