Home Overview Demos/Webs News Download/Install Docs/Support Investors/Partners Commercial
Tutorial: Single Page Interface SEO Compatible Web Site With ItsNat (Stateless version)
Introduction/Setup
How can a web site be SPI and page based at the same time?
Web application set-up
Creating the ItsNat servlet
Main page processing
Infrastructure of fundamental states
Conclusion
Download and Online Demo

Last update: 2013, Jul. 3  

Introduction


This tutorial shows the stateless web site version of this stateful version, reading first the stateful version is recommended, main ItsNat concepts are not going to be repeated in this tutorial and some parts of the stateless version are identical to stateful.

The objective of the stateless version is to make a Single Page Interface Stateless SEO Compatible website based on the stateless mode introduced in ItsNat v1.3. When we say stateless we mean there is no need of server affinity or session data sharing, therefore client requests of the same page can target any node in a cluster of symmetric nodes with the same ItsNat website.

To understand this tutorial some basic knowledge of ItsNat is required.


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 to generate the initial HTML page being sent to the client, is done after the user code in server is executed. Hence the same user 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


Use your preferred IDE to create an empty web application, this tutorial uses spitutstless as the name of the web application. The simplest way is to add to the WEB-INF/lib directory all required JAR files by ItsNat (you can find these jars in /fw_dist/lib folder of the ItsNat distribution).


Creating the ItsNat servlet


ItsNat does not impose a default servlet (in fact you can use several ItsNat servlets). Using your IDE add a new servlet class with name servlet (this name is not mandatory) in the default package (again not mandatory). Default registration in web.xml is valid, ItsNat does not require special initialization parameters or filters in web.xml.

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

http://localhost:8080/spitutstless/servlet

Because our web site is SPI we would like a prettier URL like

http://localhost:8080/spitut/

We have two options:

  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" and lazy people use this JSP to start the web application in their IDE) with this content:
        <jsp:forward page="/servlet" />
    

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.spitut.SPITutGlobalLoadRequestListener;
import org.itsnat.spitut.SPITutMainLoadRequestListener;

public class servlet extends HttpServletWrapper
{
    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");
    }
}

As you can see our servlet now inherits from HttpServletWrapper:

public class servlet extends HttpServletWrapper

This ItsNat class redirects any request to the ItsNatHttpServlet object wrapping the servlet instance.

Web application configuration is done in the standard init(ServletConfig) method, configuration is ItsNat is "classic", imperative, calling configuration methods.

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

This call is not really necessary because fast-load mode is the default, this method is called to clearly state fast-load mode is mandatory in SPI SEO compatible web sites.

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

The folder WEB-INF/pages is going to be used to save "ItsNat page templates", because our web application is SPI, only one page template is need, later another very simple page will be added to monitor page visits. Into the folder WEB-INF/fragments will be saved "page fragments", page fragments are pure HTML pages where only the content of <body> (or <head>) is used.

Following in the servlet:

        itsNatServlet.addItsNatServletRequestListener(new SPITutGlobalLoadRequestListener());

Registers a global generic request listener. For instance this listener is called when a page (=document) load is requested. Source code:

package org.itsnat.spitut;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.itsnat.core.ItsNatDocument;
import org.itsnat.core.ItsNatServletRequest;
import org.itsnat.core.ItsNatServletResponse;
import org.itsnat.core.event.ItsNatServletRequestListener;

public class SPITutGlobalLoadRequestListener implements ItsNatServletRequestListener
{
    public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
    {
        ItsNatDocument itsNatDoc = request.getItsNatDocument();
        if (itsNatDoc == null)
        {
            // Requested with a custom URL, not ItsNat standard format,
            // for instance servlet without params.
            // Internal redirection specifying the target template page:
            ServletRequest servRequest = request.getServletRequest();
            String gAnalytState = servRequest.getParameter("ganalyt_st");
            if (gAnalytState != null)
                servRequest.setAttribute("itsnat_doc_name","google_analytics");
            else
                servRequest.setAttribute("itsnat_doc_name","main");
            ServletResponse servResponse = response.getServletResponse();

            request.getItsNatServlet().processRequest(servRequest,servResponse);
        }
    }
}

If the request includes the default parameter itsnat_doc_name and the specified template exists the method ItsNatServletRequest.getItsNatDocument() returns a non-null ItsNatDocument object, if the URL requested does not include itsnat_doc_name is a "custom" request and getItsNatDocument() returns null. The later is the case of pretty URLs.

We first check whether the parameter ganalyt_st is present, this parameter is going to be used later for state monitoring using Google Analytics. If this parameter is not present is the expected case of a request like this:

http://localhost:8080/spitut/

Or this:

http://localhost:8080/spitut/servlet

In this case we have to load the main page of our web site, so itsnat_doc_name is specified with value "main" as a request attribute, the value "main" is the name of the main template of our SPI web site, ItsNat checks first if itsnat_doc_name was specified as attribute and then as parameter. Now we are ready to re-send the request to ItsNat calling:

            request.getItsNatServlet().processRequest(servRequest,servResponse);

This new request has the same effect as the URL:

http://localhost:8080/spitut/servlet?itsnat_doc_name=main

Now the target page template is specified and found and request.getItsNatDocument(); returns a non-null value and nothing is done in this case because this global listener is not the typical place to manage normal page load requests.


Main page processing


Returning to the servlet:

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

This call registers with the name "main" the page template file main.xhtml (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 Stateless SEO Compatible Web Site With ItsNat</title>
    <link rel="stylesheet" type="text/css" href="css/style.css" />
    <script type="text/javascript" src="js/spi_bookmarking.js?timestamp=2011-02-11_01"></script>
    <script type="text/javascript">
    function removeById(id)
    {
        var elem = document.getElementById(id);
        if (!elem) return;
        elem.parentNode.removeChild(elem);
    }
    function removeChildren(node)
    {
        while(node.firstChild) { var child = node.firstChild; node.removeChild(child); }; // Altnernative: node.innerHTML = ""
    }

    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;
    </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 Stateless SEO Compatible Web Site With ItsNat</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 href="?st=overview" onclick="setState('overview'); return false;"
                           class="menuLink">Overview</a>
                    </td>
                    <td id="menuOpDetailId">
                        <a href="?st=detail" onclick="setState('detail'); return false;"
                           class="menuLink">Detail</a>
                    </td>
                    <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>

</body>
</html>

Yes this is the typical pure HTML template for ItsNat including some dynamic parts, marked as "not cached" with the special attribute itsnat:nocache="true" where itsnat prefix is declared in <html> as xmlns:itsnat="http://itsnat.org/itsnat".

For instance:

    <title id="titleId" itsnat:nocache="true">Tutorial: Single Page Interface SEO Compatible Web Site With ItsNat</title>

The title of the SPI web site will change when a new fundamental state is loaded.

                <tr class="mainMenu" itsnat:nocache="true">

This row contains the web site main menu, is not cached because we need to access menu items in server to change the color of the current selected option.

        <td id="contentParentId" itsnat:nocache="true" style="padding:20px; vertical-align:super;" >

This table cell is the parent of the "content area", when the end user clicks some menu option, this zone will change accordingly.

Finally:

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

The URL of this <iframe> will be changed to keep track of fundamental states with Google Analytics.

The following 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;

Is being used to send ItsNat "stateless events", a stateless event is an extension of W3C DOM Events and is fired calling some public ItsNat methods from JavaScript. Stateless events are used in this tutorial to notify the server about the next fundamental state to be loaded, 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.

An example of how fundamental states are changed is this link (a menu item):

                        <a href="?st=overview" onclick="setState('overview'); return false;"
                           class="menuLink">Overview</a>

When end user clicks this link the call setState('overview'); sends a user event to command server to load the new fundamental state "overview", because onclick returns false the default behavior of the link is aborted hence href="?st=overview" is ignored and the page remains the same (partially changed with the new state). This is not the case of search engine crawlers like Google Search, these bots ignore JavaScript and follow the link loading a new page with "overview" as the initial state. This is an example of a dual link AJAX/normal.

Now is the time of registering a load listener for the main template (back to the servlet):

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

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 loading the page)
  2. When a stateless event has been received in server specifying the template of this document

In this tutorial the same template is going to be used for the initial load and for stateless processing, this is the simplest (not mandatory) option and in this case is appropriated also for stateless because the template is simple. Because everything is the same position in page in first load time and when processing stateless events, locById attribute is not needed in this tutorial.

The source code is really simple:

package org.itsnat.spitut;

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;

public class SPITutMainLoadRequestListener implements ItsNatServletRequestListener
{
    public void processRequest(ItsNatServletRequest request, ItsNatServletResponse response)
    {
        new SPITutMainDocument((ItsNatHttpServletRequest)request,(ItsNatHttpServletResponse)response);
    }
}

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

Unlike the stateful mode, SPITutMainDocument objects are not going to be retained in server because when used for the initial page the 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.

Source code of SPITutMainDocument:

package org.itsnat.spitut;

import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.itsnat.core.ClientDocument;
import org.itsnat.core.ItsNatServlet;
import org.itsnat.core.event.ItsNatEventDOMStateless;
import org.itsnat.core.html.ItsNatHTMLDocument;
import org.itsnat.core.http.ItsNatHttpServletRequest;
import org.itsnat.core.http.ItsNatHttpServletResponse;
import org.itsnat.core.tmpl.ItsNatDocFragmentTemplate;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.events.Event;
import org.w3c.dom.events.EventListener;
import org.w3c.dom.html.HTMLDocument;
import org.w3c.dom.html.HTMLTitleElement;

public class SPITutMainDocument implements EventListener
{
    protected ItsNatHTMLDocument itsNatDoc;
    protected String title;
    protected HTMLTitleElement titleElem;
    protected Map<String,Element> menuElemMap = new HashMap<String,Element>();
    protected Element contentParentElem;
    protected Element googleAnalyticsElem;
    protected String googleAnalyticsIFrameURL;

    public SPITutMainDocument(ItsNatHttpServletRequest request, ItsNatHttpServletResponse response)
    {
        this.itsNatDoc = (ItsNatHTMLDocument)request.getItsNatDocument();

        HTMLDocument doc = itsNatDoc.getHTMLDocument();

        this.titleElem = (HTMLTitleElement)doc.getElementById("titleId");
        this.title = titleElem.getText(); // Initial value

        menuElemMap.put("overview",doc.getElementById("menuOpOverviewId"));
        menuElemMap.put("detail",doc.getElementById("menuOpDetailId"));
        // More menu options here...

        this.contentParentElem = doc.getElementById("contentParentId");
        this.googleAnalyticsElem = doc.getElementById("googleAnalyticsId");
        this.googleAnalyticsIFrameURL = googleAnalyticsElem.getAttribute("src");  // Initial value

        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 = "overview";
            }
            else
            {
                stateName = servReq.getParameter("st");
                if (stateName == null)
                    stateName = "overview";
            }

            changeState(stateName);
        }
        else
        {
            itsNatDoc.addEventListener(this);            
        }
    }

    public ItsNatHTMLDocument getItsNatHTMLDocument()
    {
        return itsNatDoc;
    }

    public void setStateTitle(String stateTitle)
    {
        String pageTitle = stateTitle + " - " + title;
        if (itsNatDoc.isLoading())
            titleElem.setText(pageTitle);
        else
            itsNatDoc.addCodeToSend("document.title = \"" + pageTitle + "\";\n");
    }

    public Element getContentParentElement()
    {
        return contentParentElem;
    }

    public ItsNatDocFragmentTemplate getFragmentTemplate(String name)
    {
        ItsNatServlet servlet = itsNatDoc.getItsNatDocumentTemplate().getItsNatServlet();
        return servlet.getItsNatDocFragmentTemplate(name);
    }

    public DocumentFragment loadDocumentFragment(String name)
    {
        ItsNatDocFragmentTemplate template = getFragmentTemplate(name);
        if (template == null) return null;
        return template.loadDocumentFragment(itsNatDoc);
    }

    public String getFragmentName(String stateName)
    {
        String fragmentName = stateName;
        int pos = stateName.indexOf('.');
        if (pos != -1) fragmentName = stateName.substring(0, pos); // Case "overview.showpopup"
        return fragmentName;
    }

    public void changeState(String stateName)
    {    
        changeState(stateName,null);
    }
    
    public void changeState(String stateName,String stateSecondaryName)
    {
        String fragmentName = getFragmentName(stateName);

        ItsNatDocFragmentTemplate template = getFragmentTemplate(fragmentName);
        if (template == null)
        {
            changeState("not_found");
            return;
        }
        
        if (!itsNatDoc.isLoading())
        {
            ClientDocument clientDoc = itsNatDoc.getClientDocumentOwner();
            String contentParentRef = clientDoc.getScriptUtil().getNodeReference(contentParentElem);            
            clientDoc.addCodeToSend("removeChildren(" + contentParentRef + ");");  // ".innerHTML = '';"
        }
        
        //ItsNatDOMUtil.removeAllChildren(contentParentElem);

        // Setting new state:
        changeActiveMenu(stateName);

        DocumentFragment frag = template.loadDocumentFragment(itsNatDoc);
        contentParentElem.appendChild(frag);

        if (stateName.equals("overview")||stateName.equals("overview.showpopup"))
        {
            boolean popup = stateName.equals("overview.showpopup");
            new SPITutStateOverview(this,popup);
        }
        else if (stateName.equals("detail"))
        {
            new SPITutStateDetail(this,stateSecondaryName);
        }
        
        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()
    }

    public void registerState(SPITutState state)
    {
        setStateTitle(state.getStateTitle());
        String stateName = state.getStateName();
        itsNatDoc.addCodeToSend("spiSite.setURLReference(\"" + stateName + "\");");
        googleAnalyticsElem.setAttribute("src",googleAnalyticsIFrameURL + stateName);
    }

    public void handleEvent(Event evt)
    {
        ItsNatEventDOMStateless itsNatEvt = (ItsNatEventDOMStateless)evt;
        
        String stateName = (String)itsNatEvt.getExtraParam("state_name");
        String stateSecondaryName = (String)itsNatEvt.getExtraParam("state_secondary_name");
        
        changeState(stateName,stateSecondaryName);
    }

    public void changeActiveMenu(String stateName)
    {
        String mainMenuItem;
        int pos = stateName.indexOf('.');
        if (pos != -1) mainMenuItem = stateName.substring(0, pos); // Case "overview.showpopup"
        else mainMenuItem = stateName;

        for(Element menuItemElem : menuElemMap.values())
        {
            menuItemElem.setAttribute("class","foo");            
            menuItemElem.removeAttribute("class");
        }
       
        Element currentMenuItemElem = menuElemMap.get(mainMenuItem);
        currentMenuItemElem.setAttribute("class","menuOpSelected");
    }
}

This class is the core of the web site, tightly associated to the main template and responsible of (fundamental) state management, states directly dependent on main menu.

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

For instance:

        if (!itsNatDoc.isCreatedByStatelessEvent())
        {
            SAME CODE AS STATEFUL (initial state/content)       
        }
        else
        {
            itsNatDoc.addEventListener(this);            
        }

The method isCreatedByStatelessEvent() is used to distinguish load page from stateless event processing, in case of document load for processing the received stateless event we must register a global listener going to be called inmediatelly after the load phase of the document created by the stateless event just ends.

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:

        if (!itsNatDoc.isLoading())
        {
            ClientDocument clientDoc = itsNatDoc.getClientDocumentOwner();
            String contentParentRef = clientDoc.getScriptUtil().getNodeReference(contentParentElem);            
            clientDoc.addCodeToSend("removeChildren(" + contentParentRef + ");");  // ".innerHTML = '';"
        }

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: 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.

Another interesting method in stateless is:

    public void handleEvent(Event evt)
    {
        ItsNatEventDOMStateless itsNatEvt = (ItsNatEventDOMStateless)evt;
        
        String stateName = (String)itsNatEvt.getExtraParam("state_name");
        String stateSecondaryName = (String)itsNatEvt.getExtraParam("state_secondary_name");
        
        changeState(stateName,stateSecondaryName);
    }

This listener was registered in load time when the document is loaded by a stateless event, and executed just after the load phase, in this case a stateless is processed, this stateless event transport the necessary data sent by client to change the state in client, because this is the stateless event dispatching phase any change to the DOM now will generate JavaScript DOM code to do the same in client, this why is important to bring the just loaded document to the state in client before doing something in event listener phase.

Now you can understand code like this:

    public void changeActiveMenu(String stateName)
    {
        String mainMenuItem;
        int pos = stateName.indexOf('.');
        if (pos != -1) mainMenuItem = stateName.substring(0, pos); // Case "overview.showpopup"
        else mainMenuItem = stateName;

        for(Element menuItemElem : menuElemMap.values())
        {
            menuItemElem.setAttribute("class","foo");            
            menuItemElem.removeAttribute("class");
        }
       
        Element currentMenuItemElem = menuElemMap.get(mainMenuItem);
        currentMenuItemElem.setAttribute("class","menuOpSelected");
    }

This code is different to the stateful version, in this case knowing of the current selected menu in client is not important if you clear all menu nodes (before highlighting the current menu option), one call like this menuItemElem.setAttribute("class","foo"); ensures the attribute in document in memory is defined because we need to generate a removeAttribute call to be sent to the client.


Infrastructure of fundamental states


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, so we need the necessary data from client, this is the reason of the state_secondary_name parameter received for instance to insert and manage the "detail" state and "more detail" substate:


package org.itsnat.spitut;

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

public class SPITutStateDetail extends SPITutState
{
    public SPITutStateDetail(SPITutMainDocument spiTutDoc,String stateSecondaryName)
    {
        super(spiTutDoc);
        
        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");            
        }
    }

    @Override
    public String getStateTitle()
    {
        return "Detail";
    }

    @Override
    public String getStateName()
    {
        return "detail";
    }

}

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 call 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 to remove code in client because removing code is a very easy task in client, the stateless approach is ItsNat is gold to insert big and complex parts of markup in client usually mixed with data loaded in server.

The state overview including a popup is an example of how we can make a limited used of ItsNat components in stateless:


package org.itsnat.spitut;

public class SPITutStateOverview extends SPITutState
{
    public SPITutStateOverview(SPITutMainDocument spiTutDoc,boolean showPopup)
    {
        super(spiTutDoc);

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

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

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

    @Override
    public String getStateTitle()
    {
        return "Overview";
    }

    @Override
    public String getStateName()
    {
        return "overview";
    }
}

package org.itsnat.spitut;

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.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.html.HTMLBodyElement;
import org.w3c.dom.html.HTMLDocument;

public class SPITutStateOverviewShowPopup extends SPITutState
{
    protected SPITutStateOverview parent;
    protected ItsNatModalLayer layer;

    public SPITutStateOverviewShowPopup(SPITutStateOverview parent)
    {
        super(parent.getSPITutMainDocument());
        this.parent = parent;

        SPITutMainDocument spiTutDoc = parent.getSPITutMainDocument();
        ItsNatHTMLDocument itsNatDoc = parent.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
    }

    @Override
    public String getStateTitle()
    {
        return "Overview Popup";
    }

    @Override
    public String getStateName()
    {
        return "overview.showpopup";
    }

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

}

On the contrary to the stateful tutorial we are not to show here the complete code to avoid repetition, remaining parts are very similar to stateful.


Conclusion


This tutorial has shown one generic example of how to build Single Page Interface stateless SEO compatible web sites with ItsNat. Most of the code in this tutorial is infrastructure code, it could be generalized and reused in many projects, this is the fixed cost of a SPI web site.


Download, Online Demo and Links


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

Download source code and binaries

Source code in GitHub




Terms of Use Privacy Statement Contributor Agreement delicious icon Add to delicious