devxlogo

Restoring Conventional Browser Navigation to AJAX Applications

Restoring Conventional Browser Navigation to AJAX Applications

any developers have adopted AJAX as a way to develop rich web applications that are almost as interactive and responsive as desktop applications. AJAX works by dividing the web UI into different segments. A user can perform an operation on one segment and then start working on other segments without waiting for the first operation to finish.

But AJAX has a major disadvantage; it breaks standard browser behavior, such as Back, Forward, and bookmarking support. Rather than forcing users to adapt to AJAX’s shortcomings, developers should make their AJAX applications comply with the traditional web interaction style, providing the following usability features:

  • Back and Forward buttons should work, allowing end users to navigate through the history pages in an intuitive manner.
  • Users should be able to create bookmarks.
  • The Refresh button should work as expected, refreshing the current state as opposed to reinitializing the application or an unexpected page or state.
  • End users should be able to search pages using their browsers’ standard search functions.
  • Search engines should be able to index the pages in AJAX applications and create deep links to search terms.

Typical AJAX applications do not meet the first three usability features. Dynamic web applications that use AJAX technologies such as the XMLHttpRequest object and DOM updates do not update the browser history correctly, because retrieving data using a background request doesn’t change the page URL. The outcome is that when users click the Back button, they are likely to jump all the way out of the web application, losing any saved state. Clicking the Refresh button reinitializes an AJAX application to the initial state of the current page (often effectively restarting the application). Bookmarks don’t work for the same reason; using a bookmark to return to an AJAX URL might not reflect the page state at the time the user made the bookmark.

From a user’s perspective, standard navigational conventions are quite important. People have come to expect certain interaction conventions when using web applications that typical AJAX applications lack. For example, when users try to bookmark a page using the browser’s URI they lose any information about the intermediate steps (the background requests) that the AJAX application made to create that page. Similarly, when users click the browser’s Back button they will be shown the previous page, which is usually not the previous page state. The difference between “page” and “page state” is critical in AJAX applications, because they’re the same thing to users, but to browsers, they’re completely different.

For example, suppose a person’s using an AJAX-based search application. The first page contains a search form where users enter search criteria—call it Page1. After submitting the filled-out form, the person sees a page containing the first few search results (Page2), along with a pagination link to view the next set of search results on Page3, and so on. The application retrieves the next set of search results for Page3 by sending an asynchronous call and populating the DOM structure of Page2 with the retrieved data. The page state has changed, but note that the URL of the page (still Page2) has not; in other words, despite the fact that users think they’re looking at a new page (Page3) the browser’s history stack for this application looks like this:

   Page2   Page1

Therefore, when users click the Back button from Page3, they logically expect to see the first set of search results (the original Page2); however, the browser will dutifully navigate back to Page1 instead. Similarly if they refresh the page while looking at Page3 they will see Page2 instead. This counterintuitive behavior confuses users and makes applications more difficult to use.

Possible Solutions
If each unique document created by an AJAX application could be addressed by its own URL, then you could conceptually solve the broken Back button and bookmarking problem. Notice that the search application references the first result page by one URL and the second page by a different URL; in other words, the two URLs each refer to different resource documents. In contrast, AJAX documents are built up from a starting document plus an arbitrary number of DOM manipulations that add, remove, and change the nodes in a document. Therefore, to recreate an AJAX document at any given point, you have to start by retrieving the starting document’s URL, and then apply all the DOM changes made up to that point. That would be a lot simpler to do if each AJAX request had a unique URL.

See also  Custom Java Web Development - The Heartbeat of Modern Web Development

Enter the URL Hash
A URL hash is the portion of a URL that follows a number sign (#). URL hashes usually refer to specific locations within a page rather than to some other page, so they’re typically used in links to let users move easily to specific locations within long pages. But you don’t have to use them that way. Instead, you can use URL hashes to store information about the DOM manipulations in AJAX applications—essentially giving each AJAX request a unique URL.

URL hashes have two huge built-in advantages for creating AJAX history. The first is that changing the hash in the current URL won’t trigger a page reload, which provides a way to create a unique URL without changing pages. The second is that browsers already treat URL hashes as part of the page history; in other words, navigating to a URL hash adds an entry to the browser’s history list. If you add a distinct entry to the current URL after a hash mark, you can create a historical trail useful for re-enabling standard backward and forward navigation and bookmarking capability in AJAX applications. For example, you might use these URL hashes to refer to the two result pages in the search application:

   http://searchserver/search.jsp?searchText=television#page=1   http://searchserver/search.jsp?searchText=television#page=2

The preceding URL hashes (the page=1 and page=2 portions) refer to the search page the user is currently viewing.

Really Simple History (RSH)
Several frameworks are targeting the AJAX UI issues to try to maintain the browser’s default behavior in AJAX-based applications. The Really Simple History (RSH) framework is one example. This simple framework consists of two open source JavaScript classes, DhtmlHistory and HistoryStorage. These two classes make it possible for AJAX applications to support bookmarking and the Back and Forward buttons.

The DhtmlHistory class provides a history abstraction for AJAX applications. AJAX pages use the add() method to add history events to the browser, specifying new locations and associated history data. The DhtmlHistory class updates the browser’s current URL using an anchor hash, such as #new-location, and associates history data with this new URL. AJAX applications register themselves as history listeners; as the user navigates with the Back and Forward buttons, the browser fires history events that provide the browser’s new location and any history data that was persisted with an add() call.

The second class, HistoryStorage, gives developers a way to store an arbitrary amount of history data. In normal web pages when a user navigates to a new web site, the browser unloads and clears out all application and JavaScript state on the web page it’s leaving; if the user later returns to that page using the Back button, all the data is lost. The HistoryStorage class solves this problem by exposing an API containing simple hash table methods such as put(), get(), and hasKey(). Developers can use these methods to store an arbitrary amount of data that persists even after a user has left a web page. If the user later returns by clicking the Back button, code in the page can access the stored data through the HistoryStorage class. Internally, the data storage can be achieved by using a hidden form field, taking advantage of the fact that browsers autosave form field values even after a user has left the page.

Dojo to the Rescue
Dojo provides another possible solution to the AJAX navigational issues, allowing web applications to capture Back and Forward button clicks, and set a unique URL in the browser’s location field for bookmarking.

Dojo is an Open Source DHTML toolkit written in JavaScript that gives you an easy way to build dynamic capabilities into web pages (or other environments that support JavaScript). You can use Dojo’s components to make web sites more usable, responsive, and functional. With Dojo you can build complex user interfaces easily, prototype interactive widgets quickly, and animate transitions. Dojo’s low-level APIs and compatibility layers promote writing portable JavaScript and simplify complex scripts, while its event system, I/O APIs, and generic language enhancement form the basis of a powerful programming environment.

See also  Custom Java Web Development - The Heartbeat of Modern Web Development

The included dojo.undo.browser module provides access to the browser history, making it possible for users to click Back and Forward without leaving your AJAX application. The toolkit fires callback messages when Back and Forward events occur, giving developers an opportunity to update the web application appropriately. Dojo generates browser history by using a hidden IFRAME and/or by adding a unique value to the hash portion of the page URL.

Remember that changing the application state identifier in the form of a URL hash does not cause the page to refresh, so URL hashes are ideal for maintaining application state. The Dojo-generated unique hash values already support bookmarking, but you can specify more meaningful values for the application state identifier to improve bookmark readability. The Dojo state object represents the state of the page. This state object gets callbacks when users click the Back or Forward buttons. In addition to registering state objects with dojo.undo.browser directly, you can pass the state object to dojo.io.bind(), which will bind the events for you.

Configuring Dojo
To use dojo.undo.browser:

  • Define the preventBackButtonFix property as false in djConfig. This property allows Dojo to add a hidden IFRAME to the current page via a document.write() command. If you do not do this, dojo.undo.browser will not work correctly.
  • Add the appropriate require statement: dojo.require(“dojo.undo.browser”);
  • Register the initial state of the page by calling dojo.undo.browser.setInitialState(state); In that code, state is the state object that will be notified when the user clicks Back—all the way back to the start of the web application. (If users click Back once more, the browser navigates to the page that preceded the application, if any.)

The state object should have the following functions defined:

  • For receiving Back notifications: back(), backButton(), or handle(type), where type is the string “back.”
  • For receiving Forward notifications: forward(), forwardButton(), or handle(type), where type is the string “forward.”

Here’s an example of a very simple state object:

   var state = {         back: function() {          alert("Back was clicked!");       },         forward: function() {          alert("Forward was clicked!");       }   }; 

To register a state object that represents the result of a user action, use the following call:

dojo.undo.browser.addToHistory(state);

Alternatively, if you are using dojo.io.bind(), and the state object contains the function back() or backButton(), or the property changeUrl, then dojo.io.bind() will handle the calls to dojo.undo.browser for you (this only works with XMLHTTPTransport and ScriptSrcTransport).

To change the URL in the browser’s location bar, include a changeUrl property on the state object. If this property is set to true, dojo.undo.browser will generate a unique value for the fragment identifier. If it is set to any other value (except undefined, null, zero, or an empty string), then that value will be used as the fragment identifier. In other words, developers can let Dojo pick a hash value or set a custom hash value. In either case, this feature lets users bookmark the page in such a way that code can rebuild the current page state.

The Search Application
Here’s an example that shows how to apply Dojo’s history features to the search application problem mentioned earlier. Figure 1 shows the Search application directory structure:

 
Figure 1. Search Application Directory Structure: The sample search application uses these folders.
 
Figure 2. Simple Search Form: Users enter some search text and click the Search button, which sends a background request to the server.

The main page (searchMain.jsp, from which users can trigger a search) resides in the application’s root folder (see Figure 2). The dojo folder contains the standard Dojo files such as dojo.js. The script folder contains special JavaScript files for the search application, including HistoryTracker.js, which defines the application state implementation, and dojoUtility.js, which defines JavaScript utility functions such as the body onload handler function and the function for firing the search.

Author’s Note: This sample application uses JSP, but the example would work equally well with any server-side technology (PHP, ASP, etc.).
See also  Custom Java Web Development - The Heartbeat of Modern Web Development

Here’s the HTML for the

tag shown in Figure 2:

                  

Note that the HTML uses the input type of “button” (rather than using “submit”) to avoid automatic form submission. The goal is to send an asynchronous background request to fetch the search result. Therefore, the button’s onclick event calls the performSearch() method, which triggers search operation.

The performSearch() function code shown below is in a separate dojoUtility.js file:

   function performSearch(searchTxt, pageNumber) {      var bindUrl = "/dojoapp/doSearch.jsp";      if(searchTxt) {         bindUrl += "?searchTxt=" + searchTxt ;         if(pageNumber) {            bindUrl += "&pageNumber=" + pageNumber;         }         dojo.io.bind({            url: bindUrl,            load: function(type, data, evt){               dojo.undo.browser.addToHistory(                  new HistoryTracker(data, searchTxt,                   pageNumber, "searchContent"));               dojo.byId("searchContent").innerHTML = data;            }
 
Figure 3. Dummy Search Result: The elements of the search results appear at the bottom of the page.
}); } }

The performSearch() method accepts two parameters—searchTxt and pageNumber—that correspond respectively to the search text entered in the text box and the search page to which the user wants to navigate. The performSearch() function sends an asynchronous request to doSearch.jsp, which retrieves the search result. You can see a dummy search result at the bottom of Figure 3:

The code that produces the search results is:

   You have searched for:       <%= request.getParameter("searchTxt") %>   
Showing page number: <%= (request.getParameter( "pageNumber") == null) ? "1" : request.getParameter( "pageNumber") %>
', '<%= (request.getParameter("pageNumber") == null) ? "2" : (Integer.parseInt(request.getParameter( "pageNumber") ) + 1) %>');">View next set of results

This is a dummy page printing only the page number and search text. The doSearch.jsp page provides the anchor tag for fetching the next set of search results. The javascript:performSearch() function is the href location for that tag. It takes the search text and the next page number as parameter.

The performSearch() function uses the HistoryTracker object. You register the application state change by calling dojo.undo.browser.addToHistory(). To make that work, though, you need to remember to register the initial application state, which usually happens in the body onload event:

   dojo.addOnLoad(handleBodyLoad);   

The handleBodyLoad() function is defined as:

   function handleBodyLoad() {      var state = new HistoryTracker(null, null, null, "searchContent");      dojo.undo.browser.setInitialState(state);      handleUrlHash();   }

The handleBodyLoad() function creates a state object that has null values for both searchTxt and pageNumber. Then it registers the initial application state by passing that state object in the line dojo.undo.browser.setInitialState(state). Finally, it calls handleUrlHash(), which parses the URL hash and restores application state depending on the value of hash:

   function  handleUrlHash() {      var urlHash = location.hash.substring(1);      var searchText, pageNumber;         if(urlHash && urlHash != '') {         var hashParams = urlHash.split(";");            for (i = 0; i < hashParams.length; i++) {            var temp = hashParams[i].split("=");            if(temp && temp.length > 0) {               switch(temp[0]) {                  case 'searchTxt':                      searchText = temp[1];                     break;                  case 'pageNumber':                      pageNumber = temp[1];                     break;               }            }         }      }      if(searchText) {         if(pageNumber) {            performSearch(searchText, pageNumber);         }          else {            performSearch(searchText, 1);         }      }   }
 
Figure 4. AJAX Search Results: The search results appear on the same page as the search form.

The handleUrlHash() method parses the URL hash to retrieve the search text and page number and then calls performSearch() with the interpreted search text and page number. The performSearch() method shown earlier is responsible for firing the search. It calls doSearch.jsp using searchText and pageNmuber as parameters, and then displays the search results.

That completes the search application; now you can launch the search page by browsing to the URL http://yourdomain/searchMain.jsp.

When you see the main form, enter some search text (for example, “What is Dojo”) and you’ll see the search results appear on the same page, (see Figure 4).

Although the page doesn’t change, you’ll see that the application appended a URL hash to the original URL, for example:

   http://localhost:8080/dojoapp/searchMain.jsp#searchTxt=       What%20is%20Dojo;pageNumber=1

The URL hash contains semicolon-separated values for searchTxt and pageNumber. If you click the “View next set of results” link, you’ll see a second search results page.

As you can see, Dojo can make your AJAX applications respect bookmarks and the Back and Forward buttons. As illustrated in the sample application, you assign a distinct URL hash to each page, which updates the browser history. Using this technique, the Back and Forward buttons work correctly, so users can easily bookmark any search page. The downloadable code contains the ready-to-use sample application described in this article. You can reuse the ideas it exemplifies as the basis of your own navigation-aware AJAX applications.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist