How to use HTML5 History API with Nitrogen

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 &quot;-//W3C//DTD XHTML 1.0 Strict//EN&quot; &quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd&quot;>;
<;html xmlns=&quot;http://www.w3.org/1999/xhtml&quot;>;
<;head>;
<;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=UTF-8&quot; />;
<;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

About these ads
This entry was posted in Erlang, Nitrogen. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s