Over a million developers have joined DZone.

Four SPI Flavors: Stateless Using Hashbangs

How to build Single Page Interface websites using the Java-based ItsNat framework to improve SEO and load times.

· Web Dev Zone

Start coding today to experience the powerful engine that drives data application’s development, brought to you in partnership with Qlik.

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

Our objective is again to create SPI web sites, web sites with no page reloading in the same time "simulating" pages without losing the benefits of page based web sites 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 web site using a stateful (web sessions are used) and hashbang approach. In this article/tutorial we are going to do the same, but in this case using the stateless mode (introduced in ItsNat 1.3). 

When we say stateless we mean there is no need of server affinity or session data sharing, in this mode ItsNat does not use the built-in web session system of your Java app server, therefore client requests to update the single page can target any node in a cluster of symmetric nodes with the same ItsNat website.

State in client is saved again in hashbangs, in spite of History API is the perfect approach, hashbangs are virtually universal, browsers like the old Internet Explorer 8 (with a small but significative market share) does not support History API. History API will be applied in the next articles.

To better understand this article read before the previous one, some concepts are not going to explained again.

SPI, SEO Compatiblity and Stateless Relationship

The SEO compatibility of ItsNat has not very much to do with stateful or stateless modes, the key feature is fast-load mode (the default mode), when the initial page (based on a pure HTML template) is being loaded, any DOM change performed in server is not sent as JavaScript, DOM rendering executed to generate the initial HTML page being sent to the client, is done after the user code in server is executed modifying the initial DOM. Hence the same developer code manipulating DOM can generate JavaScript or plain HTML depending on the phase is executed, when an event is received (JavaScript) or on load time of the initial page (HTML). 

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 spistlesshashbangtut as the name of the web application.

Creating the ItsNat Servlet

Create a servlet as explained in the stateful hashbang tutorial.

According to this setup the URL accessing our servlet is (8080 is supposed): http://localhost:8080/spistlesshashbangtut/servlet

Because our web site is SPI we would like a prettier URL like http://localhost:8080/spistlesshashbangtut/

Configuration of web.xml is the same as stateful hashbang tutorial:

1. Add the servlet as the welcome file

<welcome-file-list>
<welcome-file>servlet</welcome-file>
</welcome-file-list>

2. Add a simple index.jsp (this file is usually by default the "welcome file") with this content:

<jsp:forward page="/servlet" />

In web.xml

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

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

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import org.itsnat.core.ItsNatServletConfig;
import org.itsnat.core.http.HttpServletWrapper;
import org.itsnat.core.http.ItsNatHttpServlet;
import org.itsnat.core.tmpl.ItsNatDocumentTemplate;
import org.itsnat.spistlesshashbangtut.SPITutGlobalLoadRequestListener;
import org.itsnat.spistlesshashbangtut.SPITutMainLoadRequestListener;

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

        ItsNatHttpServlet itsNatServlet = getItsNatHttpServlet();

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

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

        itsNatServlet.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());

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

        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");
    }
}

It is very similar to the stateful version but with some subtle differences:

  • No interest in sessions, for instance no need to specify the number of max sessions like itsNatCtx.setMaxOpenDocumentsBySession(4);.
  • No global event listener is registered, this code itsNatServlet.addEventListener(new SPITutGlobalEventListener()); is missing because the purpose of SPITutGlobalEventListener was to ask users what to do when session is lost.
  • Events of "main" documents are explicitly disabled by calling docTemplate.setEventsEnabled(false);, this is required to make this page stateless (page/document is loaded but not retained in server in sync with client), this call is needed because by default is stateful.

In this example the default URL is:

http://localhost:8080/spistlesshashbangtut/

The main page of our web site is loaded as explained in the stateful hashbang version.

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 SEO Compatible Web Site With ItsNat STATELESS Using Hashbangs
    <link rel="stylesheet" type="text/css" href="css/style.css" />
    <script type="text/javascript" src="js/spi_hashbang.js?timestamp=2015-08-18_01"></script>
    <script type="text/javascript">
    function setState(state_name,state_secondary_name)
    {
        if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded
        var userEvt = document.getItsNatDoc().createEventStateless();
        userEvt.setExtraParam('itsnat_doc_name',"main");
        userEvt.setExtraParam('state_name',state_name);
        if (state_secondary_name) userEvt.setExtraParam('state_secondary_name',state_secondary_name);
        document.getItsNatDoc().dispatchEventStateless(userEvt, 3 /*XHR_ASYNC_HOLD*/, 1000000);
    }
    window.spiSite.onBackForward = setState;

    function removeById(id)
    {
        var elem = document.getElementById(id);
        if (!elem) return;
        elem.parentNode.removeChild(elem);
    }
    </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 SEO Compatible Web Site With ItsNat STATELESS <br> Using Hashbangs</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="?st=overview">Overview</a>
                    </td>
                    <td id="menuOpDetailId">
                        <a onclick="setState('detail'); return false;" class="menuLink" href="?st=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 example, the main difference is the JavaScript code:

function setState(state_name,state_secondary_name)
{
    if (typeof document.getItsNatDoc == "undefined") return; // Too soon, page is not fully loaded
    var userEvt = document.getItsNatDoc().createEventStateless();
    userEvt.setExtraParam('itsnat_doc_name',"main");
    userEvt.setExtraParam('state_name',state_name);
    if (state_secondary_name) userEvt.setExtraParam('state_secondary_name',state_secondary_name);
    document.getItsNatDoc().dispatchEventStateless(userEvt, 3 /*XHR_ASYNC_HOLD*/, 1000000);
}
window.spiSite.onBackForward = setState;

function removeById(id)
{
    var elem = document.getElementById(id);
    if (!elem) return;
    elem.parentNode.removeChild(elem);
}

The setState method sends an "ItsNat stateless event" to server (take a look to the call dispatchEventStateless(...) call) providing the necessary info to generate a new state in server to propagate back to client. A stateless event is an extension of W3C DOM Events defined by ItsNat and is fired calling some public ItsNat methods from JavaScript like dispatchEventStateless(...).

Because the stateless nature of this example, the server does not remember any data sent by client, just generate the according Javascript code and markup to change the state in client and forget the request. The result is the same as the stateful hashbang example but with a different (stateless) approach based on the stateless capabilities of ItsNat.

Stateless events are used in this tutorial to notify the server about the next fundamental state to be loaded (in the stateful example stateful custom user events were used), any received stateless event in server will load a document specified by itsnat_doc_name parameter and will be processed by an event listener in server previously registered in the just loaded document.

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 the same as the stateful hashbang example.

In stateless the SPITutMainLoadRequestListener.processRequest(…) method will be called in two different scenarios:

  1. When the servlet receives a new load request of this template that is when initially loading the page, conventional use
  2. When a stateless event has been received in server specifying the template of this document (load phase of stateless event processing)

In this tutorial the same template is going to be used for the initial load and for stateless event processing, this is the simplest (not mandatory) option, in this case is appropriated because the template is very simple, everything is in the same position when the page is loaded and when processing stateless events, there is no need of locById attributes in this tutorial (please read first about how ItsNat support stateless apps in Manual, a tutorial about using stateless mode exists).

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.

Unlike the stateful mode, SPITutMainDocument objects are not going to be retained in server because the page template was registered "stateless" calling docTemplate.setEventsEnabled(false);, and when a SPITutMainDocument is created to dispatch a stateless event, this document object is ever stateless (not retained in server) because the nature of stateless event processing.

The class SPITutMainDocument inherits from SPIStlessHashbangMainDocument the main class of the mini-framework to create SPI SEO compatible stateless based on hashbang web sites under the package org.itsnat.spistlesshashbang.

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

  1. org.itsnat.spi: generic classes, same as all other SPI web site tutorialsClasses: SPIMainDocumentConfigSPIMainDocumentSPIStateSPIStateDescriptor 
  2. org.itsnat.spistless: stateless specificClass: SPIStlessMainDocument 
  3. org.itsnat.spistlesshashbang: stateless and hashbang processing specificClasses: SPIStlessHashbangMainDocument 

We are not going to explain the classes of the package org.itsnat.spi because they are the same as seen in stateful hashbang tutorial. Note there is no SPIStlessState class (not needed).

Main Page Processing Stateless

Now is time to step down to describe how the mini-framework manages a stateless web site.

There is only one class into the package org.itsnat.spistlessSPIStlessMainDocument.

package org.itsnat.spistless;

import org.itsnat.spi.SPIMainDocumentConfig;
import org.itsnat.spi.SPIStateDescriptor;
import org.itsnat.core.ClientDocument;
import org.itsnat.core.event.ItsNatEventDOMStateless;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spi.SPIMainDocument;
import org.itsnat.spi.SPIState;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;

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

        if (itsNatDoc.isCreatedByStatelessEvent())
        {
            EventListener listener = new EventListener()
            {
                @Override
                public void handleEvent(Event evt)
                {
                    ItsNatEventDOMStateless itsNatEvt = (ItsNatEventDOMStateless)evt;

                    String stateName = (String)itsNatEvt.getExtraParam("state_name");

                    changeState(stateName,itsNatEvt);
                }
            };

            itsNatDoc.addEventListener(listener);
        }
    }

    public SPIState changeState(String stateName)
    {
        return changeState(stateName,null);
    }

    public SPIState changeState(String stateName,ItsNatEventDOMStateless itsNatEvt)
    {
        SPIStateDescriptor stateDesc = config.getSPIStateDescriptor(stateName);
        if (stateDesc == null)
        {
            return changeState(config.getNotFoundStateName(),itsNatEvt);
        }

        // Cleaning previous state:
        if (!itsNatDoc.isLoading())
        {
            ClientDocument clientDoc = itsNatDoc.getClientDocumentOwner();
            String contentParentRef = clientDoc.getScriptUtil().getNodeReference(config.getContentParentElement());
            clientDoc.addCodeToSend("window.spiSite.removeChildren(" + contentParentRef + ");");  // ".innerHTML = '';"
        }

        // Setting new state:
        changeActiveMenu(stateName);

        String fragmentName = stateDesc.isMainLevel() ? stateName : getFirstLevelStateName(stateName);
        DocumentFragment frag = loadDocumentFragment(fragmentName);
        config.getContentParentElement().appendChild(frag);

        return createSPIState(stateDesc,itsNatEvt);
    }

    public abstract SPIState createSPIState(SPIStateDescriptor stateDesc,ItsNatEventDOMStateless itsNatEvt);


    public void changeActiveMenu(String stateName)
    {
        String mainMenuItemName = getFirstLevelStateName(stateName);

        Element currentMenuItemElem = config.getMenuElement(mainMenuItemName);
        for(Element menuItemElem : config.getMenuElementMap().values())
        {
            onChangeActiveMenu(menuItemElem,(currentMenuItemElem == menuItemElem));
        }
    }

    public abstract void onChangeActiveMenu(Element menuItemElem,boolean active);
}

Some parts of this class are very similar to the stateful example, others are very different.

For instance:

if (itsNatDoc.isCreatedByStatelessEvent())
{
    EventListener listener = new EventListener()
    {
        @Override
        public void handleEvent(Event evt)
        {
            ItsNatEventDOMStateless itsNatEvt = (ItsNatEventDOMStateless)evt;

            String stateName = (String)itsNatEvt.getExtraParam("state_name");

            changeState(stateName,itsNatEvt);
        }
    };
    itsNatDoc.addEventListener(listener);
}

The method isCreatedByStatelessEvent() is used to distinguish conventional load page from stateless event processing, in case of document load for stateless event processing we must register a listener going to be called inmediatelly after the load phase of the document created by the stateless event, because a new special stateless event is again (and automatically) fired.

Some things are different when processing a stateless event, the client page is loaded to be modified generating JavaScript in the event listener dispatch phase but the document loaded may differ from the client state, for instance, this is the reason of this DOM cleaning in client to prepare the client to receive the new inserted markup of the new state, take a look to the code of changeState():

public SPIState changeState(String stateName,ItsNatEventDOMStateless itsNatEvt)
{
    SPIStateDescriptor stateDesc = config.getSPIStateDescriptor(stateName);
    if (stateDesc == null)
    {
        return changeState(config.getNotFoundStateName(),itsNatEvt);
    }

    // Cleaning previous state:
    if (!itsNatDoc.isLoading())
    {
        ClientDocument clientDoc = itsNatDoc.getClientDocumentOwner();
        String contentParentRef = clientDoc.getScriptUtil().getNodeReference(config.getContentParentElement());
        clientDoc.addCodeToSend("window.spiSite.removeChildren(" + contentParentRef + ");");  // ".innerHTML = '';"
    }

    // Setting new state:
    changeActiveMenu(stateName);

    String fragmentName = stateDesc.isMainLevel() ? stateName : getFirstLevelStateName(stateName);
    DocumentFragment frag = loadDocumentFragment(fragmentName);
    config.getContentParentElement().appendChild(frag);

    return createSPIState(stateDesc,itsNatEvt);
}

Some explanation is needed: the method changeState is going to be called in three different phases:

  1. When the initial page loads (isLoading() is true): markup of the fragment content is inserted as markup (because fast-load mode is enabled)
  2. When the document for processing a stateless event loads (isLoading() is true): markup of the fragment content is inserted as DOM, no JS is generated in this load phase (normal behavior when processing a stateless event)
  3. When the stateless event is being dispatched by the registered event listener ((isLoading() is false): modified DOM generates JavaScript to be sent to client, this is the reason of custom code to clean the current content in client, because the no initial content is defined in the document loaded to process the stateless event.

This event ItsNatEventDOMStateless itsNatEvt is dispatched by the event listener that was registered on load time when the document is loaded by a stateless event, and executed just after the load phase, this stateless event transports the necessary data sent by the client to change the state in client, because this is the stateless event in dispatching phase any change to the DOM will generate JavaScript DOM code to do the same in client to bring the just loaded document to the required state in client.

Main Page Processing and Hashbangs

Time to another step down to know how to manage hashbangs.

The package org.itsnat.spistlesshashbang has only one class SPIStlessHashbangMainDocument, yes this class inherits from SPIStlessMainDocument and specifies how to manage hashbangs in this stateless context.

package org.itsnat.spistlesshashbang;

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.spistless.SPIStlessMainDocument;

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

        if (!itsNatDoc.isCreatedByStatelessEvent())
        {
            HttpServletRequest servReq = request.getHttpServletRequest();
            String stateName = servReq.getParameter("_escaped_fragment_"); // Google bot, has priority, its value is based on the hash fragment
            if (stateName != null)
            {
                if (stateName.startsWith("st=")) // st means "state"
                    stateName = stateName.substring("st=".length(), stateName.length());
                else // Wrong format
                    stateName = config.getDefaultStateName();
            }
            else
            {
                stateName = servReq.getParameter("st");
                if (stateName == null)
                    stateName = config.getDefaultStateName();
            }

            changeState(stateName);
        }

    }
}

Code is basically the same as stateful but the sentence if (!itsNatDoc.isCreatedByStatelessEvent()), because this code only apply to normal page loading, not for stateless event processing (even in load phase).

Everything explained in stateful hashbang tutorial about Google AJAX Crawling specification is also applied here.

Because this tutorial uses hashbangs, the JavaScript library spi_hashbang.js is the same as the stateful version.

Infrastructure of Fundamental States

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

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

package org.itsnat.spistlesshashbangtut;

import org.itsnat.spistlesshashbang.SPIStlessHashbangMainDocument;
import org.itsnat.core.event.ItsNatEventDOMStateless;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.spi.SPIMainDocumentConfig;
import org.itsnat.spi.SPIState;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.Element;

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

    @Override
    public SPIState changeState(String stateName,ItsNatEventDOMStateless itsNatEvt)
    {
        SPIState state = super.changeState(stateName,itsNatEvt);

        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 SPIState createSPIState(SPIStateDescriptor stateDesc,ItsNatEventDOMStateless itsNatEvt)
    {
        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"))
        {
            String stateSecondaryName = itsNatEvt != null? (String)itsNatEvt.getExtraParam("state_secondary_name") : null;
            return new SPITutStateDetail(this,stateDesc,stateSecondaryName);
        }
        else
            return null;
    }

    @Override
    public void onChangeActiveMenu(Element menuItemElem,boolean active)
    {
        if (active)
        {
            menuItemElem.setAttribute("class","menuOpSelected");
        }
        else
        {
            menuItemElem.setAttribute("class","foo");
            menuItemElem.removeAttribute("class");
        }
    }
}

Code is similar to the stateful version but not the same, in stateless we need to provide the necessary data from client to recreate in server the client state and perform the required operations to change the client to a new state.

For instance:

...
else if (stateName.equals("detail"))
{
    String stateSecondaryName = itsNatEvt != null? (String)itsNatEvt.getExtraParam("state_secondary_name") : null;
    return new SPITutStateDetail(this,stateDesc,stateSecondaryName);
}
...

State definition is very different in stateless, in stateful we can easily remember in server the current state in client, in stateless is not possible unless you make some custom use of session, we are not going to use sessions in no way, so we need the necessary data from client, this is the reason of thestate_secondary_name parameter received for instance to insert and manage the "detail" state and "more detail" substate, whether "more detail" must be "shown" or "hidden".

This is the detail.html link used to change from "show" to "hide" and viceversa:

<a itsnat:nocache="true" id="detailMoreId" href="javascript:;" onclick="setState('detail',this.getAttribute('action')); return false;">Hide|More Detail</a><br>

The attribute action is defined to save in client the current state of the page, and used to send to the server the current state to change accordingly. In this example we only need a second parameter, more complex seb sites will require a more complex context captured to send to server.

@Override
public void onChangeActiveMenu(Element menuItemElem,boolean active)
{
    if (active)
    {
        menuItemElem.setAttribute("class","menuOpSelected");
    }
    else
    {
        menuItemElem.setAttribute("class","foo");
        menuItemElem.removeAttribute("class");
    }
}

This code is different to the stateful version, we know the new menu item to activate but we do not know what is the current selected (remember it is stateless), therefore we clear all menu items and select the new active (this method is called by the base class for all menu items).

This is the SPITutStateDetail:

package org.itsnat.spistlesshashbangtut;

import org.itsnat.core.ClientDocument;
import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.itsnat.spi.SPIState;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateDetail extends SPIState
{
    public SPITutStateDetail(SPITutMainDocument spiTutDoc,SPIStateDescriptor stateDesc,String stateSecondaryName)
    {
        super(spiTutDoc,stateDesc,true);

        HTMLDocument doc = getItsNatHTMLDocument().getHTMLDocument();
        Element detailMoreLink = doc.getElementById("detailMoreId");

        if ("moreDetail".equals(stateSecondaryName))
        {
            DocumentFragment frag = spiTutDoc.loadDocumentFragment("detail.more");
            Element detailMoreElem = ItsNatTreeWalker.getFirstChildElement(frag);
            detailMoreElem.setAttribute("id", "detailContentId");

            Element contentParentElem = spiTutDoc.getContentParentElement();
            contentParentElem.appendChild(detailMoreElem);
            detailMoreLink.setTextContent("Hide");
            detailMoreLink.setAttribute("action","lessDetail");
        }
        else
        {
            ClientDocument clientDoc = getItsNatHTMLDocument().getClientDocumentOwner();
            clientDoc.addCodeToSend("removeById('detailContentId');");

            detailMoreLink.setTextContent("More Detail");
            detailMoreLink.setAttribute("action","moreDetail");
        }
    }
}

States are inherited from org.itsnat.spi.SPIState, no need of dispose method (necessary in stateful).

In stateless there is less automatic management of client state and some custom JavaScript is convenient, for instance:

ClientDocument clientDoc = getItsNatHTMLDocument().getClientDocumentOwner();
clientDoc.addCodeToSend("removeById('detailContentId');");

This code calls the auxiliary JavaScript method removeById defined in client to remove the "more detail" content, this is needed because by default our stateless document do not include a "more detail" fragment DOM, in practice you usually do not need automatically generated JavaScript from server to remove code in client because removing code is a very easy task in client, ItsNat helps you a lot in stateless for inserting big and complex parts of markup in client usually mixed with data loaded in server, but removing client DOM is trivial with custom JavaScript.

The state "overview" including a popup ("overview.popup") is an example of how we can make a limited use of ItsNat components also in stateless:

package org.itsnat.spistlesshashbangtut;

import org.itsnat.spi.SPIState;
import org.itsnat.spi.SPIStateDescriptor;

public class SPITutStateOverview extends SPIState
{
    public SPITutStateOverview(SPITutMainDocument spiTutDoc,SPIStateDescriptor stateDesc,boolean showPopup)
    {
        super(spiTutDoc,stateDesc,!showPopup);

        if (showPopup) showOverviewPopup();
        else cleanOverviewPopup();
    }

    public void showOverviewPopup()
    {
        new SPITutStateOverviewPopup(this);
    }

    public void cleanOverviewPopup()
    {
        SPITutStateOverviewPopup.dispose(this);
    }

}
package org.itsnat.spistlesshashbangtut;

import org.itsnat.comp.ItsNatComponentManager;
import org.itsnat.comp.layer.ItsNatModalLayer;
import org.itsnat.core.ClientDocument;
import org.itsnat.core.domutil.ItsNatTreeWalker;
import org.itsnat.core.html.ItsNatHTMLDocument;
import org.itsnat.spi.SPIMainDocument;
import org.itsnat.spi.SPIState;
import org.itsnat.spi.SPIStateDescriptor;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.html.HTMLBodyElement;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateOverviewPopup extends SPIState
{
    protected SPITutStateOverview parent;
    protected ItsNatModalLayer layer;

    public SPITutStateOverviewPopup(SPITutStateOverview parent)
    {
        this(parent,parent.getSPIMainDocument().getSPIStateDescriptor("overview.popup"));
    }

    public SPITutStateOverviewPopup(SPITutStateOverview parent,SPIStateDescriptor stateDesc)
    {
        super(parent.getSPIMainDocument(),stateDesc,true);
        this.parent = parent;

        SPIMainDocument spiTutDoc = getSPIMainDocument();
        ItsNatHTMLDocument itsNatDoc = getItsNatHTMLDocument();
        HTMLDocument doc = itsNatDoc.getHTMLDocument();
        ItsNatComponentManager compMgr = itsNatDoc.getItsNatComponentManager();
        ItsNatModalLayer layer = compMgr.createItsNatModalLayer(null,false,1,0.5f,"black",null);
        Element parentLayer = layer.getElement();
        parentLayer.setAttribute("id","overviewPopupLayerContainerId");

        HTMLBodyElement body = (HTMLBodyElement)doc.getBody();

        DocumentFragment frag = spiTutDoc.loadDocumentFragment("overview.popup");
        Element container = ItsNatTreeWalker.getFirstChildElement(frag);
        body.appendChild(container);

        container.setAttribute("id","overviewPopupContentContainerId");

        itsNatDoc.addCodeToSend("try{ window.scroll(0,-1000); }catch(ex){}");
        // try/catch is used to prevent some mobile browser does not support it
    }

    public static void dispose(SPITutStateOverview parent)
    {
        ClientDocument clientDoc = parent.getItsNatHTMLDocument().getClientDocumentOwner();
        clientDoc.addCodeToSend("removeById('overviewPopupLayerContainerId');");
        clientDoc.addCodeToSend("removeById('overviewPopupContentContainerId');");
    }
}

Remaining classes SPITutGlobalLoadRequestListenerSPITutMainLoadRequestListener are the same as stateful hashbang counterparts. In stateful has no sense a SPITutGlobalEventListener as explained before.

Conclusion

This tutorial has shown a generic example of how to build SPI stateless using hashbangs web sites 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 the demo running online (try to clear cookies to check there is no need of keep client state in server).

Download the source code in GitHub.

Create data driven applications in Qlik’s free and easy to use coding environment, brought to you in partnership with Qlik.

Topics:
itsnat ,single page interface ,seo ,java ,web dev

Published at DZone with permission of Jose Maria Arranz. See the original article here.

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

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

{{ parent.tldr }}

{{ parent.urlSource.name }}