Forward and Back Buttons with Ajax
Posted on Jun 28, 2017 by Nate in UncategorizedTL;DR
If you’re working on a site that will only use Ajax calls on some pages, but where you still want to have functional URLs, then use replaceState()
for the first URL you add to history. This will let you navigate back to the previous page, if needed. For the other Ajax calls, use pushState()
.
Intro
So, I guess 3 years is long enough between blog posts, huh? I honestly hadn’t realized that it had been so long, but that’s what happens when you’re busy (and a little lazy).
I’ve been using Ajax for years, but until recently, I’ve never really had to worry about using it in a way where keeping up with the page’s URL structure would be important. But in the project I’ve been working on lately, we decided to implement the AddSearch API, which is a really impressive service that you should check out if you’re not familiar with it. Anyway, the site that we’re working on is using Umbraco 7.5.9, and while I could have used route-hijacking (a future blog post, hopefully) to implement the API, I decided to use JavaScript and make the calls with Ajax, instead.
But when dealing with search results, being able to bookmark or email a link to some results would definitely be useful, which meant that I needed to track the URLs in the address bar, even though we wouldn’t be refreshing the page. Furthermore, when paging through results, using the browser’s forward and back buttons is simply natural behavior — we absolutely have to support that.
Thankfully, HTML5 comes to the rescue with its History API, specifically, pushState()
and replaceState()
.
HTML5 History API
The AddSearch API that I was interacting with only needed three parameters: an API key (which is irrelevant to this post), the search term(s), and the page number. While the users of the site I was developing would never see the actual URLs I would be passing to AddSearch, it made sense for the site’s search URLs to contain the parameters that I would need. So I settled on a pattern of:
http://siteurl.com/site-search?s=terms&page=1
Behind the scenes, their initial search query was being entered into a textbox, and when they pressed “submit,” I grabbed their entry, assigned it a page number of “1,” and submitted the search to the AddSearch API. Obviously, I stopped the page from reloading, and once the results were returned from the API, I added them to the page.
To update the page’s URL, we have two methods to choose from, pushState()
and replaceState()
.
When to Use Each
The best way to explain the differences between these two methods is to walk you through the rest of my example, because I really only understood it after trying it for myself.
Here’s what I wanted to happen:
- User performs search.
- Results are displayed and URL updates to http://siteurl.com/site-search?s=terms&page=1
- User clicks “next page,” sees new results, and URL updates to http://siteurl.com/site-search?s=terms&page=2
- If user clicks back button, they go back to http://siteurl.com/site-search?s=terms&page=1
- If user clicks forward button, they go back to the second page, or if they click the back button again, they go back to whatever page they were on before performing the search.
The pushState()
and replaceState()
functions both change the page’s URL without reloading the page, and they both use the same parameters. Initially, I tried to only use pushState()
. While this did allow me to navigate between the search page’s results URLs (in other words, the “fake” URLs that were being generated by my Ajax calls), I couldn’t navigate back to the pages I had viewed before those calls. So if I had run my search from http://siteurl.com/home and then gone through 3 pages of search results, I could click the “back” button through all 3 search results pages, but I couldn’t get back to the /home page.
I finally realized that for that initial search, I needed to show the first page’s results by using the replaceState()
function, and any subsequent Ajax calls should use the pushState()
function.
My Solution
I ended up creating a function to handle these history entries:
// 'addNewPushState' is set to false when "back" or "forward" buttons are clicked // because we don't want new history records created in those cases function handlePushState(terms, page, addNewPushState) { var stateObj = { sitesearch: 'page: ' + page }; var url = '/site-search?s=' + terms + '&page=' + page; if (addNewPushState) { if (isFirstVisit()) { history.replaceState(stateObj, '', url); } else { history.pushState(stateObj, '', url); } } }
Notice the isFirstVisit()
function. It dictates when we use replaceState()
and when we use pushState()
. It can tell by checking the document.referrer
object:
function isFirstVisit() { result = true; if (document.referrer.indexOf('/site-search') > -1 || (history.state != null && history.state.sitesearch != null)) { result = false; } return result; }
If document.referrer
is the /site-search page, then we know that we’ve already been looking at search results, and we need to use the pushState()
function. The same applies if the history.state
object contains a sitesearch
entry, because that’s what I decided to call the object in the handlePushState()
function that we just looked at.
The Details
Both the replaceState()
and pushState()
functions take 3 parameters: a state
object, a title (which isn’t really used at this point), and a URL.
The state object is very versatile. I only used it to store an object named “sitesearch” which contained the current page number of that Ajax call. But the state object can contain up to 640,000 characters of information, so some people actually use it to store the page’s content. Then, when “back” or “forward” is clicked, they can return the content from cache, which is typically faster than pulling the data again. Of course, there are potential pitfalls with this approach, especially if you can’t be sure how much content may have been on the page.
For my application, I didn’t mind querying the AddSearch API again, so I really just used the state object to help me verify whether or not it was my first time to edit the history.
The URL parameter is what will be shown in the browser’s address bar.
Handling Browser Navigation
The final piece was to handle the “back” and “forward” events, which I did with this function:
$(window).on('popstate', function (event) { // navigate to the url that's returned if (history.state != null && history.state.sitesearch != null) { var params = parseParams(window.location.search); prepareSearch(params['s'], params['page'], false); } else { window.location = window.location.href; } });
The “popstate” event is what’s called when “back” or “forward” is clicked in a browser. When that happens, this function first checks for a “sitesearch” object in the history. If it exists, then the browser is trying to navigate back to one of our “fake” pages generated through Ajax. So I simply re-run the Ajax call (the prepareSearch()
function) and submit “false” as that function’s final parameter to make sure we don’t make a new history record for this call.
If the “sitesearch” object is null, then the browser is trying to navigate back to whatever page it was on before we started making Ajax calls, in which case, we can actually just navigate to the URL that’s displayed in the address bar.
Final Thoughts
I wrote more in this post than I had intended. I hope it’s not too rambling, and that it will actually help someone. To me, the most important part, and the nuance that I didn’t initially get from other articles is what I cover in the “TL;DR” section at the beginning of the post. That was my “a-ha!” moment that inspired me to write this.
Nevertheless, there are some other really great resources out there if you’d like to learn more about the details of HTML5’s History API:
MDN’s main article on the History API
MDN’s “Manipulating the Browser History”
The CSS Tricks article, “Using the HTML5 History API”