One of the problems with building dynamic Ajax applications is that browser updates only parts of the page and is not changing the location, so it is difficult to give a user the link to the current state of the page. This makes it difficult to get the browser Backbutton working correctly, because if a user leaves a dynamic page and changes the URL, the Backbutton will return him to the initial state of the Ajax page and not to the state the user had when he navigated away.
Fortunately, the recent HTML5 specification defines the standard way to work with History API and allows to maintain history for Ajax applications. The history API is already available for all the HTML5 capable browsers.
There are lots of good resources that explain how History API is supposed to work, e.g. this article gives an excellent overview.
In this post I will try to explain how to use Nitrogen with History API, continuing to use tabs custom control which we built in the previous posts.
To manage the interactions with history we can use excellent library History.js. Using History.js instead of working with History API directly adds few benefits as this lib gracefully degrades to using onhashchange functionality for HTML4 browsers.
Nitrogen Elements project includes history.js in “www” directory.
Makefile from nitrogen_elements_examples copies the contents of deps/nitrogen_elements/www/* ->; priv/static/
compile: rebar compile (rm -rf priv/static/nitrogen; mkdir -p priv/static/nitrogen; \ cp -r deps/nitrogen_core/www/* priv/static/nitrogen/; \ cp -r deps/nitrogen_elements/www/* priv/static/)
We need to include reference to history.js in the html template file for Ajax page that we want to enable for history.
for our tabs demo we use nitrogen_elements_examples/priv/templates/onecolumn.html as a template.
this file include references to ‘/history/html4+html5/jquery.history.js’, ‘/history/history_helper.js’ and ‘/nitrogen/bert.js’.
<;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">; <;html xmlns="http://www.w3.org/1999/xhtml">; <;head>; <;meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />; <;title>;Nitrogen - [[[page:title()]]]<;/title>; <;link rel='stylesheet' href='/nitrogen/jquery-ui/jquery.ui.all.css' type='text/css' media='screen' charset='utf-8'>; <;link rel='stylesheet' type='text/css' media='screen' href='/jqgrid/css/ui-lightness/jquery-ui-1.9.2.custom.css' />; <;link rel='stylesheet' type='text/css' media='screen' href='/jqgrid/css/ui.jqgrid.css' />; <;script src='/nitrogen/jquery.js' type='text/javascript' charset='utf-8'>;<;/script>; <;script src='/nitrogen/jquery-ui.js' type='text/javascript' charset='utf-8'>;<;/script>; <;script src='/nitrogen/livevalidation.js' type='text/javascript' charset='utf-8'>;<;/script>; <;script src='/nitrogen/nitrogen.js' type='text/javascript' charset='utf-8'>;<;/script>; <;script src='/nitrogen/bert.js' type='text/javascript' charset='utf-8'>;<;/script>; <;script src='/history/html4+html5/jquery.history.js' type='text/javascript' charset='utf-8'>;<;/script>; <;script src='/history/history_helper.js' type='text/javascript' charset='utf-8'>;<;/script>; <;script src='/jqgrid/js/i18n/grid.locale-en.js' type='text/javascript'>;<;/script>; <;script src='/jqgrid/js/jquery.jqGrid.min.js' type='text/javascript'>;<;/script>; <;/head>; <;body>; [[[page:body()]]] <;script>; [[[script]]] <;/script>; <;/div>; <;/body>; <;/html>;
‘/history/history_helper.js’ has the following contents:
(function(window,undefined){ // Prepare var History = window.History; // Note: We are using a capital H instead of a lower h var timestamps = []; //array of uniq timestamps if ( !History.enabled ) { // History.js is disabled for this browser. // This is because we can optionally choose to support HTML4 browsers or not. return false; } // push state function pushState (title, url, anydata) { // creating uniq timestamps var t = new Date().getTime(); timestamps[t] = t; // adding to history History.pushState({timestamp:t, data:anydata}, title, url); } // assign to global pushState window.pushState = pushState; // Bind to StateChange Event History.Adapter.bind(window, 'statechange', function(){ // Note: We are using statechange instead of popstate var State = History.getState(); // Note: We are using History.getState() instead of event.state if(State.data.timestamp in timestamps) delete timestamps[State.data.timestamp]; else { // send postbacks to nitrogen page page.history_back(State.data); //console.log('backbutton fired!'); } }) })(window);
History API allows to save arbitrary parameters to the history stack and it automatically pops up the stack when Backbutton is pressed.
‘/history/history_helper.js’ is a simple API which we use to talk to history.js from our Erlang code and it includes two functions:
1. ‘Pushstate’ – saves the state of the page to the history stack – we need to call it every time we want to save a state of the Ajax page.
2. it also binds antonymous function to the ‘statechange’ event – this function is called when Backbutton is pressed, it pops up the stack and returns the page state back to erlang code via #api{} event.
You can find the example for #api{} here.
In short, when you wire #api{} event to the page:
wf:wire(#api{name=history_back, tag=f1}),
this will create a javascript function ‘history_back’. This function calls back to the Erlang page and handled by function api_event(Name, Tag, Arguments) which in our case looks like this:
api_event(history_back, _B, [[_,{data, Data}]]) -> %% ?PRINT({history_back_event, B, Data}), TabIndex = proplists:get_value(tabindex, Data), wf:wire(tabs, #tab_event_off{event = ?EVENT_TABSSHOW}), wf:wire(tabs, #tab_select{tab = TabIndex}), wf:wire(tabs, #tab_event_on{event = ?EVENT_TABSSHOW}); api_event(A, B, C) -> ?PRINT(A), ?PRINT(B), ?PRINT(C). API postbacks are handled by api_event(Name, Tag, Arguments).
So, the chain of events is the following
1. ‘/history/history_helper.js’ has function binded to ‘statechange’ event which gets triggered by Backbutton is hit.
2. this function calls page.history_back(State.data);
3. history_back function is created by wf:wire(#api{name=history_back, tag=f1}).
4. history_back calls back to Erlang function api_event which allows to handle the Backbutton event.
The rest of the code is more straightforward:
1. on the page initialization we bind to tabs control ‘tabsshow’ event
wf:wire(tabs, #tab_event_on{event = ?EVENT_TABSSHOW}),
2. This calls tabs_event function everytime we switch tabs and tabs renderes:
we catch this event and save selected tabs index and url to history state object, by calling ‘pushState’ function from ‘/history/history_helper.js’ :
tabs_event(?EVENT_TABSSHOW, _Tabs_Id, TabIndex) -> wf:wire(wf:f("pushState(\"State ~s\", \"?state=~s\", {tabindex:~s});", [TabIndex, TabIndex, TabIndex])).
now we have entry in history stack.
Now, if we press Backbutton, this will fire the event which we catch by api_event function with the
api_event handler:
api_event(history_back, _B, [[_,{data, Data}]]) -> %% ?PRINT({history_back_event, B, Data}), TabIndex = proplists:get_value(tabindex, Data), wf:wire(tabs, #tab_event_off{event = ?EVENT_TABSSHOW}), wf:wire(tabs, #tab_select{tab = TabIndex}), wf:wire(tabs, #tab_event_on{event = ?EVENT_TABSSHOW});
here we take the following steps:
1. un-bind from ‘tabsshow’ event (otherwise the next step will trigger un-nessasary saving to history stack)
2. select required tab by its index
3. bind back to ‘tabsshow’ event
I am well aware that these steps are not trivial and might be hard to follow, so I am open to suggestions how to improve it because I honestly think it is an important topic.
This example tires up few important Nitrogen concepts we discussed in previous posts and also shows how to use #api{} event.
As always, the entire code example in available here