How to maintain browser history while doing a location.reload() on popstate event of Javascript

Viewed 915

So, I have a scenario where I am generating all my URL's using history.pushState function. I am NOT using any server side technique for this. I am fetching all the data only once from the server and then generating the URL using Javascript.

Now the problem that I am encountering is using the popstate function which basically mimics the back button behavior of a browser.

Everything is working fine for ONLY one back button click.

    $(window).on('popstate', function (event) {
        location.reload(true);
        //window.location.href = document.location;
        //console.log("location: " + document.location);
    });

After one back button event, I am unable to see any previous history of the browser. I am assuming it is happening because of location.reload method. I tried storing all my location also and then doing a redirect but same thing, the browser history gets lost after one refresh.

Is there a way where I can achieve this without the browser losing it's history for multiple back button clicks?

If there is a JQuery option for this, then it would be great if someone could share their knowledge on this aspect.

2 Answers

I wrote a simple plugin built around the History API that I use in my projects. It will do what you are asking.

        // instantiate the QueryString Class.            
        qsObj=new QueryString({

            onPopstate: function(qsParams, data, dataAge, e) {
                // This function is called on any window.popstate event
                //   -- triggered when user click browser's back or forward button.
                //   -- triggered when JS manipulates history.back() or history.forward()
                //   -- NOT triggered when any of the QueryString methods are called (per the native behavior of the HTML5.history API which this class uses.)
                //   -- Might be triggered on page load in some browsers.  You can handle this by checking for the existence of data, eg:  if(data){ 'do something with the data' }else{ 'move along nothing to do here' }

                console.log('executing onPopstate function');
                console.log('page query string parameters are: ', qsParams);
                console.log('stored page data is: ', data);
                console.log('dataAge is:', dataAge, ' seconds old');

                // do stuff..

                if(data) {
                    if(dataAge && dataAge<=20) {
                        // use the stored data..

                    } else {
                        // it's old data get new..
                        
                    }
                } else {
                    // the current page has no stored data..
                }
            },

            onDocumentReady: function(qsParams) {
                // Document ready is only called when a page is first browsed to or the page is refreshed.
                // Navigating the history stack (clicking back/forward) does NOT refire document ready.
                // Use this function to handle any parameters given in the URL and sent as an object of key/value pairs as the first and only parameter to this function.

                console.log('executing onDocumentReady function');
                console.log('page query string parameters are:',qsParams);

                // do stuff..
            }

        });

Plugin Usage:

// == QueryString class =============================================================================================
//
//    Modifies the query string portion of a browsers URL, updates the browsers history stack - saving page data 
//    along with it, all without reloading the page.
//    Can be used to simply obtain querystring parameter values as well.    
//
//    == Instantiate: ================================
//
//    var qsObj=new QueryString(
//
//        onPopstate: function(qsParams, data, dataAge, e) {
//
//            // This function is called on any window.popstate event
//            //   -- triggered when user click browser's back or forward button.
//            //   -- triggered when JS manipulates history.back() or history.forward()
//            //   -- NOT triggered when any of the QueryString methods are called (per the native behavior of the
//            //      HTML5.history API which this class uses.)
//            //   -- Might be triggered on page load in some browsers.  You can handle this by checking for the 
//            //      existence of data, eg:  
//            //            if(data){ 'do something with the data' }else{ 'move along nothing to do here' }
//
//            // -- qsParams: is an object that contains the current pages query string paramters as key:value pairs.
//            // -- data: is an object that contains any page data that was stored at the time that this page was added
//            //    to the history stack via this class (or any HTML5 history API method),  otherwise this value is NULL!
//            // -- dataAge:  null if data is null or the page data was not added via the methods in this class, 
//            //    otherwise the value is the age of the data in seconds.
//            // -- e: the event object if you want it.
//
//            if(data){
//                if(dataAge && dataAge <= 600){ // do it this way otherwise you'll error out if dataAge is null.
//                    // There is data and it is less than 10 minutes old.
//                    // do stuff with it..
//                }
//            }
//        },
//
//        onDocumentReady: function(qsParams){
//            // Document ready is only called when a page is first browsed to or the page is refreshed.
//            // Navigating the history stack (clicking back/forward) does NOT refire document ready.
//            // Use this function to handle any parameters given in the URL and sent as an object of key/value pairs as 
//            // the first and only parameter to this function.
//
//            // do stuff with any qsParams given in the URL..
//
//        }
//
//    });
//
//
//    == The following methods are available: =======================================
//
//
//    var qsParams = qsObj.parseQueryString(); // returns an object that contains the key/value pairs of all the query 
//                                             // string parameter/values contained in the current URL, or an empty object
//                                             // if there are none.
//
//
//    qsObj.update({
//
//        // Use this method to add/remove query string parameters from the URL and at the same time update, or add to, the 
//           browser history stack with the ability to also save page data in with the history.
//
//        opType: 'auto',
//        //  -- Optional. Allowed values: ['replace'|'push'|'auto'], Default is 'auto' unless 'push' or 'replace' is 
//        //     specifically given.
//        //  -- 'push':    Adds the new URL and any page data onto the browser history stack.
//        //  -- 'replace': Overwrites the current page history in the stack with the new URL and/or page data
//        //  -- 'auto':    compares the initial qs parameters with the updated qs parameters and if they are the same 
//        //      does a 'replace', if they are different does a 'push'.
//
//        qsParams: {
//            hello: 'world',
//            another: 'pair'
//        },
//        //  -- Optional. Object that contains key/value pairs to add to the query string portion of the URL.
//        //  -- Will entirely replace what is/isn't currently in the query string with the given key/value pairs.
//        //  -- The parameters contained in the url querystring will be made, or unmade, based on the key/value pairs 
//        //     included here so be sure to include all of the pairs that you want to show in the URL each time.
//
//
//        data: {
//           key1: 'value1',
//           key2: 'value2'
//        }
//        // Optional, Object that contains key/value pairs to store as page data in the browser history stack for this page.
//
//        // ** If qsParams and data are ommitted then nothing silently happens. (This is not the same as given but empty, 
//        //    in which case something happens.)
//
//    });
//
//
//    qsObj.Clear({
//
//       // Use this method to remove all query string parameters from the URL and at the same time update, or add to, the
//       // browser history stack with the ability to also save page data in with the history.
//
//       optype: 'auto' // optional, defaults to auto.
//
//       data: {} // Optional, defaults to empty object {}.
//    });
//
// =========================================================================================================================

Plugin Code:

; (function () {
var Def = function () { return constructor.apply(this, arguments); };
var attr = Def.prototype;

//== list attributes
attr.popstateCallback = null;
attr.docreadyCallback = null;
attr.skipParseOnInit = false;
attr.currentQS;

//== Construct
function constructor(settings) {
    if (typeof settings === 'object') {
        if ('onPopstate' in settings && typeof settings.onPopstate === 'function') {
            this.popstateCallback = settings.onPopstate;
        }
        if ('onDocumentReady' in settings && typeof settings.onDocumentReady === 'function') {
            this.docreadyCallback = settings.onDocumentReady;
        }
    }
    if (this.skipParseOnInit !== true) {
        this.currentQS = this.parseQueryString();
    }
    var self = this;
    if (typeof this.popstateCallback === 'function') {
        $(window).on('popstate', function (e) {
            var data = null;
            var dataAge = null;
            if (typeof e === 'object' && 'originalEvent' in e && typeof e.originalEvent === 'object' && 'state' in e.originalEvent && e.originalEvent.state && typeof e.originalEvent.state === 'object') {

                data = e.originalEvent.state;
                if ('_qst_' in data) {
                    dataAge = ((new Date).getTime() - e.originalEvent.state['_qst_']) / 1000; // determine how old the data is, in seconds
                    delete data['_qst_'];
                }
            }
            var qsparams = self.parseQueryString();
            self.popstateCallback(qsparams, data, dataAge, e);
        });
    }

    if (typeof this.docreadyCallback === 'function') {
        $(document).ready(function () {
            self.docreadyCallback(self.currentQS);
        });
    }
}

//== Define methods ============================================================================

attr.parseQueryString = function (url) {
    var pairs, t, i, l;
    var qs = '';
    if (url === undefined) {
        var loc = window.history.location || window.location;
        qs = loc.search.replace(/^\?/, '');
    } else {
        var p = url.split('?');
        if (p.length === 2) {
            qs = p[1];
        }
    }
    var r = {};
    if (qs === '') {
        return r;
    }
    // Split into key/value pairs
    pairs = qs.split("&");
    // Convert the array of strings into an object        
    for (i = 0, l = pairs.length; i < l; i++) {
        t = pairs[i].split('=');
        var x = decodeURI(t[1]);
        r[t[0]] = x;
    }
    return r;
};

//-- Get a querystring value from it's key name         
attr.getValueFromKey = function (key) {
    var qs = this.parseQueryString();
    if (key in qs) {
        return decodeURIComponent(qs[key].replace(/\+/g, " "));
    } else {
        return null;
    }
};

//-- if urlValue is given then qsParams are ignored.    
attr.update = function (params) {
    if (typeof params !== 'object') { return; }

    var urlValue = null;
    var data = {};
    if ('data' in params) {
        data = params.data;
        urlValue = '';
    }

    var opType = 'opType' in params ? params.opType : 'auto';

    if ('urlValue' in params && typeof params.urlValue === 'string') {
        urlValue = params.urlValue;
        if (opType === 'auto') {
            var loc = window.history.location || window.location;
            if (loc.protocol + '//' + loc.host + loc.pathname + loc.search === urlValue || loc.pathname + loc.search === urlValue) {
                opType = 'replace'; // same URL, replace
            } else {
                opType = 'push'; // different URL, push
            }
        }
    } else if ('qsParams' in params && typeof params.qsParams === 'object') {

        var pairs = [];
        for (var key in params.qsParams) {
            pairs.push(key + '=' + params.qsParams[key]);
        }
        urlValue = '?' + pairs.join('&', pairs);
        if (opType === 'auto') {
            if (this.compareQsObjects(params.qsParams, this.currentQS) === false) { // different                    
                this.currentQS = params.qsParams;
                opType = 'push';
            } else { // same                    
                opType = 'replace';
            }
        }
    }
    this.replaceOrPush(urlValue, data, opType);
};

//== Add querystring
//-- just an alias to update
attr.add = function (params) {
    return this.update(params);
};

//== Remove specified querystring parameters
//   -- Use clear() method to remove ALL query string parameters
attr.remove = function (params) {
    var urlValue = null;
    var qsChanged = false;
    if ('qsParams' in params && params.qsParams.length > 0) {
        var qs = this.parseQueryString();
        var key;
        for (var i = 0, l = params.qsParams.length; i < l; ++i) {
            key = params.qsParams[i];
            if (key in qs) {
                delete qs[key];
                qsChanged = true;
            }
        }
    }
    if (qsChanged === true) {
        var pairs = [];
        for (key in qs) {
            pairs.push(key + '=' + qs[key]);
        }
        urlValue = '?' + pairs.join('&', pairs);
        var data = 'data' in params ? params.data : {};
        var opType = 'opType' in params ? params.opType : '';
        this.replaceOrPush(urlValue, data, opType);
    }
    return;
};

//== Delete querystring
//-- just an alias to remove
attr.delete = function (params) {
    return this.remove(params);
};

//== Removes all query string parameters.
//   Use remove() method to remove just the given parameters 
attr.clear = function (params) {
    params = typeof params === 'undefined' ? {} : params;
    var urlValue = window.history.location || window.location;
    urlValue = urlValue.protocol + '//' + urlValue.host + urlValue.pathname;
    var data = 'data' in params ? params.data : {};
    var opType = 'opType' in params ? params.opType : '';
    this.replaceOrPush(urlValue, data, opType);
    return;
};

//== Simple wrapper to the HTML5 history API's replaceState() method.
//   --also used internally
attr.replaceState = function (urlValue, data) {
    if (typeof urlValue !== 'string') {
        return;
    }
    if (typeof data !== 'object') {
        data = {};
    }
    data['_qst_'] = (new Date).getTime(); // store a timestamp value        
    history.replaceState(data, '', urlValue);
};

//== Simple wrapper to the HTML5 history API's pushState() method.
//   --also used internally
attr.pushState = function (urlValue, data) {
    if (typeof urlValue !== 'string') {
        return;
    }
    if (typeof data !== 'object') {
        data = {};
    }
    data['_qst_'] = (new Date).getTime(); // store a timestamp value        
    history.pushState(data, '', urlValue);
};

//-- internal use - simple gatekeeper to decide if there is anything to do and will default to 'replace' opType if this value is not given.
attr.replaceOrPush = function (urlValue, data, opType) {
    // is there anything to do?
    if (typeof urlValue === 'string' || typeof data === 'object') {
        // yes, what type of operation are we going to do?
        if (opType === 'push') {
            this.pushState(urlValue, data);
        } else {
            this.replaceState(urlValue, data);
        }
    }
    return;
};

// == internal use - compares the existing qs with a potentially updated one to see if they are the same (returns true) or not (returns false)
attr.compareQsObjects = function (a, b) {
    if (typeof a === 'object' && typeof b === 'object') {
        var aa = [];
        var bb = [];
        for (k in a) {
            aa.push(k + a[k]);
        }
        aa.sort();
        for (k in b) {
            bb.push(k + b[k]);
        }
        bb.sort();
        if (aa.join('') !== bb.join('')) { return false; }
        return true;
    }
    return null;
};

//unleash your class
window.QueryString = Def;

})();

Well, this is not really possible. The only trick I'm thinking about is to change window.location.href when the popstate event is fired, and to pass your history as URL parameters.

$(window).on('popstate', function (event) {
    window.location.href = 'https://here.com?history = ' + yourHistory;
});

You need to figure out how you can send your history with this method, and handle it when you arrive on the new page to be able to send it again when the user clicks back button again. You also have to make sure the URL generated is not too long.

Related