Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Four SPI Flavors: Stateful Using History API

DZone's Guide to

Four SPI Flavors: Stateful Using History API

Learn how to create single page interfaces (SPI), this time with a focus on employing the History API.

Free Resource

Modernize your application architectures with microservices and APIs with best practices from this free virtual summit series. Brought to you in partnership with CA Technologies.

This is the third of a four-article series about how to build Single Page Interface (SPI) public websites SEO compatible built with the Java-based ItsNat web framework.  

Our objective is again to create SPI websites, websites with no page reloading in the same time "simulating" pages without losing the benefits of page based websites like bookmarking, Search Engine Optimization (SEO) or extreme accessibility (JavaScript disabled) according to The Single Page Interface Manifesto.

In the first one, we explained how to create a simple SPI website using a stateful (web sessions are used) and hashbang approach. The second article explained a similar approach also using hashbangs but following the stateless approach introduced in ItsNat 1.3 (session is not used).  In this third article/tutorial we are back to the stateful mode but now we introduce the History API as the technique of managing URLs and stay Single Page.

To understand this tutorial some basic knowledge of ItsNat is required, some things shown in previous articles are not repeated.

The History API is a modern JavaScript specification that allows programmatic manipulation of the page/URL history of your browser. The History API is extremely useful in a Single Page Interface SEO compatible context because it provides the finest, elegant, user transparent and near perfect way of simulating page based websites and at the same time avoiding page reload, that's it, SPI SEO compatible. End users are not going to see a big difference between the real page based and the History API version, or yes, they are going to appreciate some big visual differences like a better performance when changing states (pages) because only the changed part is loaded, everything else remains the same and in the same positions and states and the typical blank flash in page transitions disappears. History API works fine when you click on back/forward buttons, just change the browser URL but no page reload is done, this URL change can be detected by JavaScript code and custom behavior can be executed to bring the current page to the required back or forward state with no new page load.

The only caveat of History API is browser support, some old browsers like IE until v10 do not support this specification. This is not a problem, this tutorial shows how we can run the same website in browsers with no History API support just accepting conventional page navigation for fundamental states in these old browsers. In summary, if you want pure SPI SEO compatible on virtually any browser use hashgbangs, if you want perfect SPI SEO compatible use History API as shown in this tutorial accepting the experience is not the best (page based) in old browsers (going to disappear anytime soon).

SPI, SEO Compatibility, and History API

SPI SEO compatibility is really easy using History API (and ItsNat), by using History API we can change the URL of the current page with no reload, period, by this way end users "see" the browser URL changing and the page content changing in the same time, but the content changed can be done with partial markup loaded on demand by an AJAX event instead of conventional page loading. End users just feel how much the experience is improved (better performance, no blank flash when switching pages, drop down menus do not disappear...). Because the new URL is pretty and conventional (no hashbangs are used), it can be bookmarked as usual, if you are able to load the correct initial page when a concrete URL is manually written in your browser, everything is done, no _escaped_fragment_ trick is necessary, web crawlers see your site as a conventional paged site.

Web Application Set-up

ItsNat does not require special set up or application servers, any servlet container supporting Java 1.6 or upper is enough. Use your preferred IDE to create an empty ItsNat web application, this tutorial uses spistfulhsapitut as the name of the web application.

Creating the ItsNat Servlet

Create a servlet as explained in stateful hashbang tutorial.

According to this setup the URL accessing our servlet is (8080 is supposed):

http://localhost:8080/spistfulhsapitut/servlet

Because our website is SPI we would like a prettier URL like

http://localhost:8080/spistfulhsapitut/

In practice states are going to be defined in URL following this pattern:

http://localhost:8080/spistfulhsapitut/somestate

We need to map /somestate to the servlet /servlet:

<servlet-mapping>
    <servlet-name>servlet</servlet-name>
    <url-pattern>/somestate</url-pattern>
</servlet-mapping>

Yes this is a bit tedious, in this example we are going to add a fixed prefix /site:

http://localhost:8080/spistfulhsapitut/site/somestate

By using this prefix we only need to register one mapping:

<servlet-mapping>
    <servlet-name>servlet</servlet-name>
    <url-pattern>/site/*</url-pattern>
</servlet-mapping>

This is the final web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <servlet>
        <servlet-name>servlet</servlet-name>
        <servlet-class>servlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>servlet</servlet-name>
        <url-pattern>/servlet</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>servlet</servlet-name>
        <url-pattern>/site/*</url-pattern>
    </servlet-mapping>

    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>

    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>

</web-app>

Also add a simple index.jsp with this content:

<%
    response.addDateHeader("Expires", 0);
    response.addHeader("Cache-Control", "no-store,no-cache,must-revalidate,post-check=0,pre-check=0");

    response.sendRedirect("site/overview");
%>

"overview" will be the default state.

Now replace the generated code of the servlet class with this code:

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.itsnat.core.ClientErrorMode;
import org.itsnat.core.ItsNatServletConfig;
import org.itsnat.core.ItsNatServletContext;
import org.itsnat.core.http.HttpServletWrapper;
import org.itsnat.core.http.ItsNatHttpServlet;
import org.itsnat.core.tmpl.ItsNatDocumentTemplate;
import org.itsnat.spistfulhsapitut.SPITutGlobalEventListener;
import org.itsnat.spistfulhsapitut.SPITutGlobalLoadRequestListener;
import org.itsnat.spistfulhsapitut.SPITutMainLoadRequestListener;

public class servlet extends HttpServletWrapper
{
    @Override
    public void init(ServletConfig config) throws ServletException
    {
        super.init(config);

        ItsNatHttpServlet itsNatServlet = getItsNatHttpServlet();

        ItsNatServletContext itsNatCtx = itsNatServlet.getItsNatServletContext();
        itsNatCtx.setMaxOpenDocumentsBySession(4); // To avoid abusive users

        ItsNatServletConfig itsNatConfig = itsNatServlet.getItsNatServletConfig();
        itsNatConfig.setFastLoadMode(true); // Not really needed, is the same as default

        itsNatConfig.setClientErrorMode(ClientErrorMode.NOT_CATCH_ERRORS);

        String pathBase = getServletContext().getRealPath("/");
        String pathPages =     pathBase + "/WEB-INF/pages/";
        String pathFragments = pathBase + "/WEB-INF/fragments/";

        itsNatServlet.addEventListener(new SPITutGlobalEventListener());
        itsNatServlet.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());

        ItsNatDocumentTemplate docTemplate;
        docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html",pathPages + "main.html");
        docTemplate.addItsNatServletRequestListener(new SPITutMainLoadRequestListener());

        docTemplate = itsNatServlet.registerItsNatDocumentTemplate("google_analytics","text/html",pathPages + "google_analytics.html");
        docTemplate.setScriptingEnabled(false);

        // Fragments
        itsNatServlet.registerItsNatDocFragmentTemplate("not_found","text/html",pathFragments + "not_found.html");
        itsNatServlet.registerItsNatDocFragmentTemplate("overview","text/html",pathFragments + "overview.html");
        itsNatServlet.registerItsNatDocFragmentTemplate("overview-popup","text/html",pathFragments + "overview_popup.html");
        itsNatServlet.registerItsNatDocFragmentTemplate("detail","text/html",pathFragments + "detail.html");
        itsNatServlet.registerItsNatDocFragmentTemplate("detail-more","text/html",pathFragments + "detail_more.html");
    }
}

Take a look at the convention "overview-popup", in History API examples the character - will be used to show a state-substate, the character - is prettier in conventional URLs than the character . used in hashbangs ("overview.popup").

The classes SPITutGlobalEventListenerSPITutGlobalLoadRequestListenerSPITutMainDocumentSPITutMainLoadRequestListenerSPITutStateDetailSPITutStateOverviewSPITutStateOverviewPopup , are basically the same as stateful hashbang counterparts, almost only state names are changed now using - like "overview-popup" instead of . ("overview.popup").

Main Page Processing

Returning to the servlet:

ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html", pathPages + "main.html");

This call registers with the name "main" the page template file main.html (saved in WEB-INF/pages/):

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:itsnat="http://itsnat.org/itsnat">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta http-equiv="Pragma" content="no-cache" />
    <meta http-equiv="expires" content="Wed, 1 Dec 1997 03:01:00 GMT" />
    <meta http-equiv="Cache-Control" content="no-cache, must-revalidate" />
    <title id="titleId" itsnat:nocache="true">Tutorial: Single Page Interface STATEFUL SEO Compatible Web Site With ItsNat Using History API
    <link rel="stylesheet" type="text/css" href="../css/style.css" />
    <script type="text/javascript" src="../js/spi_hsapi.js?timestamp=2015-08-18_01"></script>
    <script type="text/javascript">
    function setState(name)
    {
        if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded
        var itsNatDoc = document.getItsNatDoc();
        var evt = itsNatDoc.createUserEvent("setState");
        evt.setExtraParam("name",name);
        itsNatDoc.dispatchUserEvent(null,evt);
    }
    window.spiSite.onBackForward = setState;
    </script>

< /head>
<body>

    <div class="main">
    <table style="width:100%; height:100%; padding:0; margin:0;" border="0px" cellpadding="0" cellspacing="0">
    <tbody>
    <tr style="height:50px;">
        <td>
            <h2 style="text-align:center;">Tutorial: Single Page Interface STATEFUL SEO Compatible Web Site With ItsNat <br> Using History API</h2>
        </td>
    </tr>
    <tr style="height:40px;">
        <td>
            <table style="width:100%; margin:0; padding:0; border: #ED752A solid; border-width: 0 0 2px 0; ">
                <tbody>
                <tr class="mainMenu" itsnat:nocache="true">
                    <td id="menuOpOverviewId">
                        <a onclick="setState('overview'); return false;" class="menuLink" href="overview">Overview</a>
                    </td>
                    <td id="menuOpDetailId">
                        <a onclick="setState('detail'); return false;" class="menuLink" href="detail">Detail</a>
                    </td>
                    <td> 
                </tr>
                </tbody>
            </table>
        </td>
    </tr>
    <tr style="height:70%; /* For MSIE */">
        <td id="contentParentId" itsnat:nocache="true" style="padding:20px; vertical-align:super;" >

        </td>
    </tr>
    <tr style="height:50px">
        <td style="border-top: 1px solid black; text-align:center;">
            SOME FOOTER
        </td>
    </tr>
    </tbody>
    </table>

</div>

<iframe id="googleAnalyticsId" itsnat:nocache="true" src="?ganalyt_st=" style="display:none;" ></iframe>

<!-- For Google Analytics, needed to know how the site is reached by users -->
<script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

  ga('create', 'UA-2924757-6', 'auto');
  ga('send', 'pageview');

</script>

</body>
< /html>

This template is very similar to the stateful hashbang example, but there are some important differences (beyond the title):

...
<link rel="stylesheet" type="text/css" href="../css/style.css" />
<script type="text/javascript" src="../js/spi_hsapi.js?timestamp=2015-08-18_01">
...

                <td id="menuOpOverviewId">
                    <a onclick="setState('overview'); return false;" class="menuLink" href="overview">Overview</a>
                </td>
                <td id="menuOpDetailId">
                    <a onclick="setState('detail'); return false;" class="menuLink" href="detail">Detail</a>
                </td>
...

Now the base path is http://localhost:8080/spistfulhsapitut/site/, this is the reason of "../css/style.css" and ="../js/spi_hsapi.js".

The JavaScript part of the SPI mini-framework for History API (stateful or stateless) is spi_hsapi.js, this is the source code:

function LocationState()
{
    this.removeHash = removeHash;
    this.getURL = getURL;
    this.setURL = setURL;
    this.getStateName = getStateName;
    this.setStateName = setStateName;
    this.isStateNameChanged = isStateNameChanged;

    this.url = window.location.href;

    function removeHash(url)
    {
        var pos = url.lastIndexOf('#');
        if (pos == -1) return url;
        url = url.substring(0,pos);
        return url;
    }

    function getURL() { return window.location.href; }
    function setURL(url) { window.location.href = url; }

    function getStateName()
    {
        var url = this.getURL();
        url = this.removeHash(url);
        var posR = url.lastIndexOf("/");
        if (posR == -1) return null;
        var stateName = url.substring(posR + 1);
        if (stateName == "") return null;
        return stateName;
    }

    function setStateName(stateName)
    {
        var url = this.getURL();
        url = this.removeHash(url);
        var posR = url.lastIndexOf("/");
        var url2;
        if (url.length > posR + 1) url2 = url.substring(0,posR + 1);
        else url2 = url;
        url2 = url2 + stateName;
        if (url == url2) return;

        if (window.history.pushState)
            window.history.pushState(null, null, url2);
        else
        {
            if (window.location.href != url2)
                window.location.href = url2;
        }
    }

    function isStateNameChanged(newUrl)
    {
        var url = this.getURL();
        url = this.removeHash(url);
        newUrl = this.removeHash(newUrl);
        if (newUrl == url) return false;
        var posR = url.lastIndexOf("/");
        if (posR == -1) return false;
        var posR2 = newUrl.lastIndexOf("/");
        if (posR2 == -1) return false;
        if (posR != posR2) return false;
        var stateName = url.substring(posR + 1);
        var newStateName = newUrl.substring(posR + 1);
        if (stateName == newStateName) return false;
        return true;
    }
}

function SPISite()
{
    this.load = load;
    this.detectURLStateChange = detectURLStateChange;
    this.detectURLStateChangeCB = detectURLStateChangeCB;
    this.setStateInURL = setStateInURL;
    this.removeChildren = removeChildren;
    this.onBackForward = null; // Public, user defined

    this.firstTime = true;
    this.url = null;
    this.disabled = false;

    this.load();

    function load() // page load phase
    {
        if (this.disabled) return;
    }

    function setStateInURL(stateName)
    {
        if (this.disabled) return;

        var currLoc = new LocationState();
        currLoc.setStateName(stateName);

        this.url = currLoc.getURL();

        if (!this.firstTime) return;
        this.firstTime = false;

        this.detectURLStateChange();
    }

    function detectURLStateChange()
    {
        var onpopstateSupport = ("onpopstate" in window); // Supported in IE 10
        if (onpopstateSupport)
        {
            var func = function()
            {
                arguments.callee.spiSite.detectURLStateChangeCB();
            };
            func.spiSite = this;
            window.addEventListener("popstate", func, false);
        }
    }

    function detectURLStateChangeCB()
    {
        // Detecting when only the state of the reference part of the URL changes
        var currLoc = new LocationState();
        if (!currLoc.isStateNameChanged(this.url)) return;

        // Only changed the state in reference part
        this.url = currLoc.getURL();

        var stateName = currLoc.getStateName();
        if (this.onBackForward) this.onBackForward(stateName);
        else try { window.location.reload(true); }
             catch(ex) { window.location = window.location; }
    }

    function removeChildren(node) // used by spistless
    {
        while(node.firstChild) { var child = node.firstChild; node.removeChild(child); }; // Altnernative: node.innerHTML = ""
    }
}

window.spiSite = new SPISite();

Take a look at the code using History API like window.history.pushState(null, null, url2); or window.addEventListener("popstate", func, false);

Finally, take a look at this dual link:

<td id="menuOpOverviewId">
    <a onclick="setState('overview'); return false;" class="menuLink" href="overview">Overview</a>
</td>

As you can see, the href attribute is very simple, when JavaScript is ignored (crawlers) this link just adds the state to the URL base (which ends with "/site/").

Back to the servlet, we also register a load listener for the main template:

ItsNatDocumentTemplate docTemplate;
docTemplate = itsNatServlet.registerItsNatDocumentTemplate("main","text/html", pathPages + "main.html");
docTemplate.addItsNatServletRequestListener(new SPITutMainLoadRequestListener());

The SPITutMainLoadRequestListener class is very similar to the stateful hashbang example, but we are now using the "-" separator:

package org.itsnat.spistfulhsapitut;

import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import org.itsnat.core.event.ItsNatServletRequestListener;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spi.SPIMainDocumentConfig;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.Document;
import org.w3c.dom.html.HTMLTitleElement;

public class SPITutMainLoadRequestListener implements ItsNatServletRequestListener
{
    @Override
    public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
    {
        Document doc = request.getItsNatDocument().getDocument();

        SPIMainDocumentConfig config = new SPIMainDocumentConfig();
        config.setStateNameSeparator('-')
              .setTitleElement((HTMLTitleElement)doc.getElementById("titleId"))
              .setContentParentElement(doc.getElementById("contentParentId"))
              .setGoogleAnalyticsElement(doc.getElementById("googleAnalyticsId"))
              .addMenuElement("overview",doc.getElementById("menuOpOverviewId"))
              .addMenuElement("detail",doc.getElementById("menuOpDetailId"))
              .addSPIStateDescriptor(new SPIStateDescriptor("overview","Overview",true))
              .addSPIStateDescriptor(new SPIStateDescriptor("overview-popup","Overview Popup",false))
              .addSPIStateDescriptor(new SPIStateDescriptor("detail","Detail",true))
              .addSPIStateDescriptor(new SPIStateDescriptor("not_found","Not Found",true))
                  // Remember to add fundamental (that is bookmarkable) states also to web.xml
              .setDefaultStateName("overview")
              .setNotFoundStateName("not_found");

        new SPITutMainDocument((ItsNatHttpServletRequest)request,(ItsNatHttpServletResponse)response,config);
    }
}

A new SPITutMainDocument instance is created per call (load request), this instance holds an ItsNatHTMLDocument object which represents the client document (page) being loaded as usual.

The class SPITutMainDocument inherits from SPIStfulHsapiMainDocument the main class of the mini-framework to create SPI SEO compatible stateful based on history API websites under the package org.itsnat.spistlfulhsapi.

Again we are using a mini-framework in this tutorial:

  1. org.itsnat.spi: generic classes, same as all other SPI website tutorialsClasses: SPIMainDocumentConfigSPIMainDocumentSPIStateSPIStateDescriptor 
  2. org.itsnat.spistful: stateful specific, already seen in stateful hashbang tutorialClasses: SPIStfulMainDocumentSPIStfulState
  3. org.itsnat.spistfulhsapi: stateful and history api processing specific

    Classes: SPIStfulHsapiMainDocument 

We are not going to explain the classes of the packages org.itsnat.spi and org.itsnat.spistful because they are the same as seen in stateful hashbang tutorial and already explained.

Main Page Processing and History API

Time to know how to manage the History API.

The package org.itsnat.spistfulhsapi has only one class SPIStfulHsapiMainDocument, this class inherits from SPIStfulMainDocument and specifies how to manage history API in this stateful context.

package org.itsnat.spistfulhsapi;

import org.itsnat.spi.SPIMainDocumentConfig;
import javax.servlet.http.HttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spistful.SPIStfulMainDocument;

public abstract class SPIStfulHsapiMainDocument extends SPIStfulMainDocument
{
    public SPIStfulHsapiMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response,SPIMainDocumentConfig config)
    {
        super(request, response,config);

        String stateName;
        HttpServletRequest servReq = request.getHttpServletRequest();
        String reqURI = servReq.getRequestURI();

        if (!reqURI.endsWith("/"))
        {
            // Pretty URL case
            int pos = reqURI.lastIndexOf("/");
            stateName = reqURI.substring(pos + 1); // "/name" => "name"
        }
        else
        {
            stateName = config.getDefaultStateName();
        }


        changeState(stateName,request,response);
    }
}

Obtaining the specified state present at the end of the URL on load time is really easy, no need of _escaped_fragment_ stuff like in hashbangs.

This is the first time we use the JavaScript library spi_hsapi.js, this is the source code:

function LocationState()
{
    this.getURL = getURL;
    this.setURL = setURL;
    this.getStateName = getStateName;
    this.setStateName = setStateName;
    this.isStateNameChanged = isStateNameChanged;

    this.url = window.location.href;

    function getURL() { return window.location.href; }
    function setURL(url) { window.location.href = url; }

    function getStateName()
    {
        var url = this.getURL();
        var posR = url.lastIndexOf("/");
        if (posR == -1) return null;
        var stateName = url.substring(posR + 1);
        if (stateName == "") return null;
        return stateName;
    }

    function setStateName(stateName)
    {
        var url = this.getURL();
        var posR = url.lastIndexOf("/");
        var url2;
        if (url.length > posR + 1) url2 = url.substring(0,posR + 1);
        else url2 = url;
        url2 = url2 + stateName;
        if (url == url2) return;

        if (window.history.pushState)
            window.history.pushState(null, null, url2);
        else
        {
            if (window.location.href != url2)
                window.location.href = url2;
        }
    }

    function isStateNameChanged(newUrl)
    {
        var url = this.getURL();
        if (newUrl == url) return false;
        var posR = url.lastIndexOf("/");
        if (posR == -1) return false;
        var posR2 = newUrl.lastIndexOf("/");
        if (posR2 == -1) return false;
        if (posR != posR2) return false;
        var stateName = url.substring(posR + 1);
        var newStateName = newUrl.substring(posR + 1);
        if (stateName == newStateName) return false;
        return true;
    }
}

function SPISite()
{
    this.load = load;
    this.detectURLStateChange = detectURLStateChange;
    this.detectURLStateChangeCB = detectURLStateChangeCB;
    this.setStateInURL = setStateInURL;
    this.removeChildren = removeChildren;
    this.onBackForward = null; // Public, user defined

    this.firstTime = true;
    this.url = null;
    this.disabled = false;

    this.load();

    function load() // page load phase
    {
        if (this.disabled) return;
    }

    function setStateInURL(stateName)
    {
        if (this.disabled) return;

        var currLoc = new LocationState();
        currLoc.setStateName(stateName);

        this.url = currLoc.getURL();

        if (!this.firstTime) return;
        this.firstTime = false;

        this.detectURLStateChange();
    }

    function detectURLStateChange()
    {
        var onpopstateSupport = ("onpopstate" in window); // Supported in IE 10
        if (onpopstateSupport)
        {
            var func = function()
            {
                arguments.callee.spiSite.detectURLStateChangeCB();
            };
            func.spiSite = this;
            window.addEventListener("popstate", func, false);
        }
    }

    function detectURLStateChangeCB()
    {
        // Detecting when only the state of the reference part of the URL changes
        var currLoc = new LocationState();
        if (!currLoc.isStateNameChanged(this.url)) return;

        // Only changed the state in reference part
        this.url = currLoc.getURL();

        var stateName = currLoc.getStateName();
        if (this.onBackForward) this.onBackForward(stateName);
        else try { window.location.reload(true); }
             catch(ex) { window.location = window.location; }
    }

    function removeChildren(node) // used by spistless
    {
        while(node.firstChild) { var child = node.firstChild; node.removeChild(child); }; // Altnernative: node.innerHTML = ""
    }
}

window.spiSite = new SPISite();

Code is similar to spi_hashbang.js in this case we look for the state at the end of the URL after the last character "/" and History API is used to change the URL without reloading (pushState) and to manage back/forward buttons that are manual history navigation in general (popstate event):

function LocationState()
{
    ...

    function setStateName(stateName)
    {
        ...

        if (window.history.pushState)
            window.history.pushState(null, null, url2);
        else
        {
            if (window.location.href != url2)
                window.location.href = url2;
        }
    }
    ...
}

function SPISite()
{
    ...

    function detectURLStateChange()
    {
        var onpopstateSupport = ("onpopstate" in window); // Supported in IE 10
        if (onpopstateSupport)
        {
            var func = function()
            {
                arguments.callee.spiSite.detectURLStateChangeCB();
            };
            func.spiSite = this;
            window.addEventListener("popstate", func, false);
        }
    }
    ...
}

window.spiSite = new SPISite();

If History API is not supported (old browsers), conventional page navigation is used:

if (window.history.pushState)
     ...
else
{
    if (window.location.href != url2)
        window.location.href = url2;
}

Infrastructure of Fundamental States

Now is the time to deep inside a concrete example using our mini-framework. We must define the fundamental states being used as examples in this SPI website.

This is the source code of the concrete SPITutMainDocument inherited from SPIStfulHsapiMainDocument:

package org.itsnat.spistfulhsapitut;

import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spistfulhsapi.SPIStfulHsapiMainDocument;
import org.itsnat.spi.SPIMainDocumentConfig;
import org.itsnat.spi.SPIStateDescriptor;
import org.itsnat.spistful.SPIStfulState;
import org.w3c.dom.Element;

public class SPITutMainDocument extends SPIStfulHsapiMainDocument
{
    public SPITutMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response,SPIMainDocumentConfig config)
    {
        super(request,response,config);
    }

    @Override
    public SPIStfulState changeState(String stateName,ItsNatHttpServletRequest request,ItsNatHttpServletResponse response)
    {
        SPIStfulState state = super.changeState(stateName,request,response);

        itsNatDoc.addCodeToSend("try{ window.scroll(0,-5000); }catch(ex){}");
        // try/catch is used to avoid exceptions when some (mobile) browser does not support window.scroll()

        return state;
    }

    @Override
    public SPIStfulState createSPIState(SPIStateDescriptor stateDesc,ItsNatHttpServletRequest request,ItsNatHttpServletResponse response)
    {
        String stateName = stateDesc.getStateName();
        if (stateName.equals("overview")||stateName.equals("overview-popup"))
        {
            boolean popup = false;
            if (stateName.equals("overview-popup"))
            {
                popup = true;
                stateDesc = getSPIStateDescriptor("overview");
            }
            return new SPITutStateOverview(this,stateDesc,popup);
        }
        else if (stateName.equals("detail"))
            return new SPITutStateDetail(this,stateDesc);
        else
            return null;
    }


    @Override
    public void onChangeActiveMenu(Element prevActiveMenuItemElem,Element currActiveMenuItemElem)
    {
        if (prevActiveMenuItemElem != null)
            prevActiveMenuItemElem.removeAttribute("class");
        if (currActiveMenuItemElem != null)
            currActiveMenuItemElem.setAttribute("class","menuOpSelected");
    }

}

The code is basically the same as SPITutMainDocument of the stateful hashbang example, the main difference is the use of "-" separator instead "." like in hashbangs.

All other classes of the package org.itsnat.spistfulhsapitut are the same as the stateful hashbang example, again now the "-" separator is used.

Conclusion

This tutorial has shown a generic example of how to build SPI stateful using History API websites with ItsNat similar to page based counterparts without sacrificing the typical features of the page paradigm like bookmarking, SEO, JavaScript disabled, Back/Forward buttons (history navigation), page visit counters etc. Use the mini-framework to build your SPI sites using this approach if you want.

Download, Online Demo, and Links

See it running online (try to clear cookies to check there is no need of keep client state in the server).

Download source code from GitHub.

The Integration Zone is proudly sponsored by CA Technologies. Learn from expert microservices and API presentations at the Modernizing Application Architectures Virtual Summit Series.

Topics:
itsnat ,single page interface ,java

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}