Navigation in the modern web. In-page navigation and state management with Ignite UI.

Nikolay Alipiev / Thursday, January 9, 2014

After reading this blog, you will be able to integrate the Ignite UI igGrid control with history.js, a popular library for browser history manipulation and navigation. This will allow you to navigate through igGrid states and to directly access a certain state. With the following configuration, the igGrid control will be adapted to respond to the needs of the modern browsers’ history navigation. At the same time, using the rich event set of the Ignite UI controls, you will be able to use the same configuration and integration with the rest of the Ignite UI widgets. Firstly I will provide some background on the functionality the browsers provide for page navigations and possible issues you may experience. 

HTML5 History and Navigation

Building a web site, where a site entity or a site resource is identified with individual address in the address bar, is not something new, but with HTML5 and SPA applications on track, the methods for achieving this have changed. The HTML5 Session history and navigation functionality proposes new API and flexibility, and of course some new challenges that need to be addressed.

Instead of rendering a new page on each user navigation action, the websites today, are making asynchronous requests to load the desired content within the same page the user has already loaded – data transfer is smaller, performance is better and the overall user experience is more complete. In a Single Page Application (SPA), when the asynchronous request is made, everything happens “behind the scenes”, and most of the time the user will not even notice that a request to the server has been made. If we don’t manipulate the browser URL, then it also will stay the same, even after our users perform some navigation. For example if you open a page and after that you have several asynchronous requests for data initiated by user actions, then when the user presses the back button they will leave the page, because the browser has remembered only the state when the page has been loaded, and the previous browser state is the previous page. This means that the back and forward buttons are useless in our application and the only entrance to the page will be the home page. The solution for this is to use the HTML5 Session history and navigation functionality. Its API provides functionality that allows you to manually add entities (some object representation of a current state) to the browser history and in addition to change the page URL and title without the need of request to the server. This will allow the developer of a SPA or any asynchronous application to have navigation and identification of the application’s resources and states.

Following the HTML5 specifications, the usage of browser history API seems pretty straightforward:

  • window.history – the browser history object that holds all the functionality.
  • go(), back(), forward() – methods that allows to navigate through the browser history (available in the HTML4)
  • popstate – event that is fired, when navigation in the browser happens (when back or forward buttons are pressed)
  • pushState(), replaceState() – methods that allows you to add new or replace existing state in the browser history stack.
  • state – get current state
  • length – get length of the browser history stack

Before explaining the browser history workflow, I want to point out the pushState() method and its parameters, because it’s “the man” that do most of the work. The following pictures depicts what the results will be when the pushState() method is executed:

JavaScript
  1. history.pushState({ page: 2 }, "Page 2", "?page=2");

As you see the URL and the title of the page are new and the achieved result is the same as loading a new page, even though it’s only JavaScript executed on the client. Below is a more detailed description of the method parameters:

  • State object – a representation of the current state. In our case it’s an object with one property “page” – { page: 1 }. Here you can save all the parameters that will help you to describe and recognize the current state. Note that later, when the browser is back at that state of the history, you will be able to get it using history.state.
  • Title - the new page title that appears on the page tab.
  • URL – the suffix that is added to the URL, after the domain name. This URL is the identifier of the current state/page. In this tutorial we are using the old query approach, with the parameters encoded in the URL. This may not be the best approach, but is good for easily presenting and distinguishing the several states in the URL.

Let’s imagine that user loads the initial page of your SPA by typing in the address bar of the browser. A full request is made, the page is loaded and the page state information is added to the browser history stack – all that is performed by the browser. Then the user clicks an item on the page, asynchronous request is made and some new content is loaded. Now comes the place where you can use the pushState() method. You decide, what the appropriate representation that describes the currents state would be, you give the page new title and URL and you add it to the browser history stack. What you have as a result is a “fake new page load” – the page content, title and URL have changed, but the request itself is asynchronous – faster and lighter. Your new page state is identified in the web and added to the browser history. It can be saved to browser favorites and the URL link can be sent. In addition the page will be easily indexed by the search engines.

What can eat your time, in some of the cases, is to assemble a more complicated URL. If you want to encode a lot information in the URL, then you will need some more logic to create that. Note that pushState() method will work also if you don’t have URL as a parameter. In this case you will be able to navigate back and forward in the browser, but page will not be identified with its own URL, which means it cannot be directly accessed from the browser.

What happens when the back button is pressed? Then the browser returns the previous page URL and title, and fires the popstate event. What you need to do is to attach to that event and inside the handler to try to recover the previous page content. This can happen by using the information of the current state (taken from history.state property), that you previously saved with pushState() in the state object. Let’s put some overview and see if this is enough to start using the browser history navigation functionality.

Third-party Browser History Libraries and history.js

As I already said, navigating in a web page and identifying its resources is a straightforward task using HTML5. But what about the old browsers that don’t support HTML5? This appears to be a problem. Following the official Mozilla document: Chrome 18+, Firefox 4.0+, IE 10+, Opera 11.5+, Safari 6.0 are the browser that support history functionality. This means that we should search for some third-party library that will provide us with backward compatibility and will help us to create navigation in SPA even in the old browsers that don’t support HTML5 features.

What I can add as another alert is that inside the history object, we have access only to the current state – it is not possible to take the state at two or three steps behind. We will need this functionality if our application support more complex navigation. In addition to that we need to distinguish if the back or forward button is pressed. These concerns, along with the backward computability integration, are the most important that needs to be taken into account, when we decide to choose a third-party library. We decided to choose history.js for couple of reasons – it is available for both pure JavaScript and jQuery (this fits the Ignite UI needs) as well as other popular libraries like Dojo, ExtJS, MooTools; it’s API is almost the same as the native browser API for the history – the history object is replaced with History and there are several additional methods; following the commitment in GitHub, there are recent commits and it’s community managed. Here is a broader list of popular libraries:

  • history.js
  • HTML5 History API
  • jQuery BBQ
  • Sammy.JS
  • YUI 2 - integrated
  • Angular.js - integrated

I want to note that some of the libraries are especially made for navigation and routing – history.js, HTML5 History API, jQuery BBQ or Sammy.js. On other hand most of the SPA frameworks, like Angular.js, have their own navigation or routing and if you want to start your application from scratch then these are most probably the better choice for you. But if you have already implemented most of the page, then using history.js can be better choice. And if you are still asking yourself “Which framework should I choose?” – I can answer you – “It depends”. Maybe the right choice in 10 years will be only one – choose the native browser history API, but now it really depends on your needs. For example there is a problem with jQuery BBQ and jQuery 1.10, which I’m sure will be fixed, but if you’re using jQuery 1.10 in your application then BBQ is probably not the best choice for you. When you look at the commits in the GitHub for history.js and BBQ, you will see recent commits for the one and not so recent for the other. Another thing that I think is a history.js plus, is that the main idea behind it is that it “fakes” the HTML5 history navigation feature, while jQuery BBQ and Sammy.js use the hash technique and use the “hashchange” event. The history.js library is really closer to the native browser behavior, with the following differences:

  • History is the main object (history, with small “s”, is the native one).
  • statechange event fires instead of popstate.
  • statechange fires also when a new element is added using pushState() state. In native API, popstate is fired only when back or forward button is pressed.
  • new methods for accessing all the saved history states - savedStates.
  • instead of moving the reference back/forward in the stack, a new element is pushed on the stack every time the back or forward button is pressed.

Next I will show how to recover the state of the igGrid. I will show you how to add the current igGrid paging state to the browser history and then how to recover it when the back button is pressed. In addition we will look at the difference when the forward button is pressed. To achieve complete navigation experience we will parse a given URL and will “read” a current igGrid state and will load it.

Add igGrid State to Browser History

I will skip the igGrid initialization part. If you want to get familiar with that subject, you can take look at the sample that I am using as a reference - History.js integration.  Our start point is the pageIndexChanged event, where we can get the information about the current page:

JavaScript
  1. pageIndexChanged: function (e, args) {
  2.     var pageIndex = args.pageIndex + 1,
  3.         state = { key: "page", value: pageIndex };
  4.     pushToBrowserHistory(state, null, "?page=" +  pageIndex));
  5. }

To change a page, the grid either performs an asynchronous request to get the data for the next page (remote paging) or takes the page data from the client (local paging). When the pager is interacted with the pageChaning event is fired, and when the page is changed – pageChanged is fired. We can choose either one of the events, and the only reason for choosing the one with “ed” suffix, is that it ensures that the operation has completed – the current state will be added to the browser history stack, only when it is fully finished. As you see from the code you already changed the title to “Page 2” and the URL to “?page=2”. In the sample and in this topic the parameters are encoded in the URL in a familiar manner – query string.

It’s becomes a bit more complicated when we have several igGrid features we want to push the state of to the history stack with several parameters. For example paging with multiple sorting requires multiple parameters for one state and multiple states for one feature. This is one of the ways to be implemented – we use ampersand as a feature separator, semi-colon as state separatory, and underscore as a parameter delimiter:

?page=2&sort=id_descedning;name_descending

Looking inside the grid events we see that, each event parameters represents some state of the feature. In some of the features, like paging, it’s one parameter – the current page. In features like sorting, there are parameters for the name of the column and for the sorting direction. For some features we may need three or more parameters. It seems that saving the parameters in an array is one good solution.

Recover igGrid State on Browser Navigation

Having the state in the history stack, now we should care to recover the state when the back button is pressed. What we do is to attach to the history.js “statechange” event and inside we recover the grid state by calling the appropriate API method of the current feature.

JavaScript
  1. window.History.Adapter.bind(window, "statechange", function (e, args) {
  2.     state = window.History.getState().data;
  3.     $("#grid1").igGridPaging("pageIndex", value);
  4. }

As you can see from the code, the paging feature exposes a method that allows us to change the page. It accepts the same parameters that were taken from the grid paging event. This shows some advantages of the igGrid design and for every feature we have such a design – event with arguments that represent the current state of the grid feature and corresponding API method that accepts the same parameters as the arguments. But is this enough to recover properly the igGrid state?

Let’s imagine that we change the grid page from 1 to 4. Then the state of 4 will be added to the browser history. Then we sort a grid column and sorting state object will be added to the browser history too. What we will happen if you press the back button? The previous state will return the grid to page 1. But in addition to that we should unsort the column that we previously sorted. The problem however is that we cannot concurrently access the current state and the preceding sorting one – all this information should be already embedded in the current paging state. This is achieved by adding the sort current state, before we add the page current state:

JavaScript
  1. columnSorting: function (e, args) {
  2.     var currState = window.History.getState();
  3.     undoData = { key: "sort", value: [ args.column, args.oldValue ], undo: true };
  4.     currState.data.undoData = undoData;
  5.     window.History.replaceState(currState.data);
  6. }

Because the columnSorting event happens before the columnSorted one, then when we take the current state, it will still be the paging one. In this way we will in addition add the sorting undo state to the paging state, and when we reach a stage when a back button is pressed and we recover the page state, we will have information about the sorting too.  This will happen using the following code:

JavaScript
  1. window.History.Adapter.bind(window, 'statechange', function (e, args) {
  2.     undoState = window.History.getState().data.undoState;
  3.     if(undoState.undo) {
  4.         $("#grid1").igGridSorting("unsortColumn", column);
  5.     }
  6. }

This is the way we handle more complex cases with multiple features integrated with each other. Of course this logic is needed, because we are trying to recover different states, when navigation happens, by using information from browser history stack. There is another option – when the back or forward button is pressed, to parse the URL and load the grid state from scratch. But this approach is not recommended and can create performance issues, due to the fact that we will call several grid methods which will likely re-render the igGrid several times.

Till now we talked only about the back button, but the same functionality is valid when the forward button is pressed. For security reasons, there is no way to distinguish the two in any browser. This means that the same logic will be done either on back or forward action. This may create some overhead, because when the forward button is pressed, even we have undoState we don’t need to recover it. This is the main difference from clicking the back button. So we should either leave it executed twice or search for alternative way. Such solution can be to check if this functionality is already implemented and step over it.

Loading igGrid State from URL Data

Of course we can add more details on the way the gird state is recovered, but these are the most important things that needs to be taken into account. One last thing that is left to be done is to try to load the grid state directly from the URL. Even though we cover this at the end, this is supposed to happen after the grid initialization. Imagine that you are receiving a URL that contains some encoded state, but at that stage you don’t have a state in the history stack. What you can do is to parse that URL, get the current state and using the grid API to recover that state. It is similar implementation to the one that is executed when the grid state is recovered in the statechage event, with the only difference that the state is taken from the URL instead of the browser history stack.

Summary

Let’s summarize the steps to implement browser history navigation with the igGrid control:

  1. Include history.js script.
  2. Configure the igGrid to meet the needs of your scenario.
  3. Ensure to load the initial state of the grid, by preliminary parsing the URL.
  4. Inside the –ing event handler save the current undo state, if this is necessary.
  5. Inside the –ed event handler, push the state to the browser history stack – current state, page title, page URL.
  6. Attach to the history.js statechange event and inside the handler, recover the grid state.

You can see all those steps implemented in the igGrid History.js integration sample. In this sample several grid features are enabled and you can see how it’s implemented – how the URL is formed, how undo and previous states are created, which grid methods are used for recovering the control state.

Have fun using the history.js framework and the Ignite UI toolbox.

Special thanks to Konstantin Dinev, who helped me with editing and constructing the blog.

Resources

Session history and navigation specification

History.js on GitHub

Manipulating the browser history - MDN

Manipulating History for fun & profit

Ignite UI History.js integration