-
Notifications
You must be signed in to change notification settings - Fork 13
Robust Support for Browser History in AJAX Pagination
AJAX Pagination has excellent support for browser history. It is very robust robust and mimics the behaviour of the browser history system on static pages. It is not easy to implement the following of links via AJAX, and still tick all the boxes which makes the browser history glitch free. However, AJAX Pagination does all this for you very effectively.
As many of you may know, when a link is followed, AJAX code is executed which requests new content, and changes the URL in the address bar using the history API in modern browsers. However, often, the browser history is missed. When the browser back button is used, the browser will fire the popstate event, and expect that javascript code will change the page content as necessary. This is often not too difficult to do, until there is more than one place where content may change - then it becomes tricky.
Perhaps it would be useful to know what used to happen, and other inferior solutions. Of course, if AJAX code is written specifically for changing the content in a single section of the page, it is easy to use the URL to update the content. This was the approach taken by the original code which predates AJAX Pagination. However, since packaging the code to create this gem, the idea of multiple sections, possibly nested, became an important feature which the gem had to support.
Initially, when creating the gem, I attempted to keep track of what URL was loaded into what section. That way when the history back and forward buttons are used, the transition between two sets of content can be made in the appropriate section. In fact, I did not discover another similar gem PJAX until later. The first implementation was not unlike the way PJAX implemented it. That is, you store this information in the state of the browser history API. However, the problem is that the state storage is good for a particular history item. What I was trying to store was the transition. The first instinct was to store this information, not just in the new history item, but also in the one before that. This is in fact the implementation of PJAX at the time of writing this.
Yet, it is not the correct implementation. This is because as soon as you have more than one section, you will run into glitches. You might add a few history items, and go back a few pages in the history, then go forward. Except, it will choose the wrong section to load content into. To see this, suppose there were two sections in the page - A and B, and they are both on the first bit of content. In effect, you have A1B1.
Then, you load a few pages, and have the following history: A1B1, A2B1, A2B2. Of course, when we create these items, we also want to remember which sections were changed. So you will say A1B1 (A changed), A2B1 (B changed), A2B2 (B changed). You are on page A2B2, and you click back. The new page is A2B1, and you notice that it says B changed. So, the gem will reload content in section B. Go back again, to A1B1, and content is reloaded in section A. That is correct so far. But when you click forward, you go to A2B1, and it says B changed - and that is the wrong section.
This can be quite complex. But nevertheless, it is the wrong solution. Therefore, this solution was never released.
The main problem with the solution above is that the HTML5 history API only returns the state of the new item/page to be displayed from history. In fact, further problems appear when we consider that a user, as well as going back and forwards in history one item at a time, can also jump between items.
As such, instead of allowing the information to be stored in the history API, the next solution I tried was to store it all in a javascript array. Then, we can simply store the index into that array in the history API. Then, we can say, whatever index we jump to in history, we consider the index we were on, and the index we need to change to. All the transitions can therefore be completed. This can be done by going through the array, one item at a time from starting index to ending index, changing the content in the appropriate section. Since we can see the whole array, this is easy.
Unfortunately, it is still the wrong solution. The reason is that if the page is refreshed, the javascript array data is lost. The browser will still try to call the javascript code through the popstate event when going back in history. Basically, it still remembers that all these items were created using javascript, with the pushState function, and therefore, should be restored using popstate.
This solution was again not released, and was before the release of v0.0.1.
I did consider storing the whole array in every single state. Except I was not willing to do that, because it would use up space proportional to the square of the length of history. That means if your history had 100 items, it would need to store 10,000 bits of information, just because of the replication. It would have been just too inefficient.
The next solution implemented was actually used from v0.0.1 to v0.3.0.
Because the above solutions had glitches, I could not release them. Therefore, the solution used was to create a :reload option. Using this, a user can specify when the content should be reloaded. Each section has its own :reload option.
The idea was, if the URL changes, it may or may not mean that the section should be reloaded. However, generally, there is a certain part of the URL which determines this. For example, for pages of records, the URL might have a querystring parameter ?page=1. For site navigation, the part of the URL which determines whether the section needs reloading, might be everything in the path, but not including the querystring parameters.
Then, when jumping between history items, we look at the original URL, and the new URL. For each section, we compare the relevant parts of the URL, and if it needs reloading, we reload it.
This has no problem with using the history API, since all that is required is the original URL and the new URL. No information in between is required.
It also meant that AJAX Pagination can play well with other javascript code using the history API. Except, there was a problem, in that browsers implemented it slightly differently. So I decided to use History.js with jQuery. It does mean other code will probably also need to use History.js instead of the browser's native history API, but it ensures everything fits together without problems.
The problem with the reload solution, is that a user might need to deal with an additional complication. In reality, the default could be used a simple situations. However, I want an easy to use gem. If it is possible to simplify the user experience, it probably should be done - unless of course, if it causes glitches.
As I have been involved in the IOI (International Olympiad in Informatics) as a past contestant, I can think of new ways to work this out. And the new solution, as of v0.4.0, is very good. You do not need to worry about the :reload option, and it has no glitches. If you just want to use this gem, you do not have to keep reading. If you are interested in how it works, read on.
The idea is that each section has times when its content changes, and times when its content does not change, between history items. You can keep track of the original state, and the history API gives you information on the new state (when we jump to another item in history). Using these two pieces of information, we need to know if it needs reloading. The simplest way is to use integers. If it needs to change, they are different. Otherwise they are the same.
What this means is that every time a history item is added, we give it the same integer, or we give it the next integer (which will be a unique integer - just add 1 to the previous integer).
However, the key part is that each section needs its own sequence of integers. So your state object includes an integer for each page section. Actually, initially, nothing is stored in the state, but if a section does not have an integer, we assume that it is 0.
So when you first load the page (which has sections A and B), the state object contains (A=0,B=0). When you go to another page in section A, then say another page in section B, the history stack will contain (A=0,B=0),(A=1,B=0),(A=1,B=1). When jumping, we just check if the A integer has changed, to load content in section A. Similarly for B.
The great thing is that if the page is refreshed, the browser will store the state information, whereas javascript variables are wiped. Also, each section has its own sequence. This means no bugs will appear due to interaction of sections.
The behaviour of this is the same way browsers act for static pages - except for one slight exception. Obviously when refreshing, a browser does not add to history. Interestingly, when following a link to the same URL as the current page is on, no item is added to history either.
Then, there is the browser dependent behaviour, where if you go to page A (the first visit), then page B, then page A. When jumping from the second visit of page A to the first visit of page A, Firefox will not refresh, but Chrome will.
Nevertheless, AJAX Pagination is tending towards the Firefox implementation. Basically, if the URL is the same, AJAX Pagination will not add a history item when following a link, or when jumping between history items, it will not reload any section.
However, there is a slight caveat. What if the URL changes when changing between history items, but the section does not actually have changing content? This can occur because the change in URL may actually be for another section. There is completely no obligation or necessity to change the default behaviour, which is to reload the content, even if the content is actually the same.
However, I repurposed the :reload option on a section to be used to improve the behaviour, if you want. It basically has the same syntax, but instead of the original use, now determines what parts of the URL need to be considered. If those parts are identical, the two URLs being considered are considered the same, only for the purpose of that particular section.
The new v0.4.0 has a brilliant new implementation to solve the history problem - it has no glitches when you have multiple sections, it is efficient, and it matches browser behaviour well.
And if some other javascript code needs to use the history API. There is no problem, it just needs to use History.js, and AJAX Pagination will be uneffected. AJAX Pagination stores data in an ajax_pagination object inside of the state object, and does not delete anything else in the state object, so shared use is completely possible.