Generating Tabs

Creating a component to generate tabs based on what's in its body is a bit trickier. Here is how JumpStart's TabGroup component does it...

  1. In beginRender(), TabGroup does not yet know what is in its body, so it pushes a TabTracker into the Environment.
  2. As the body renders, each Tab (inside SourceCodeTab) records a label and markup in TabTracker instead of in the DOM.
  3. In afterRenderBody(...), TabGroup pops the TabTracker from the Environment
  4. TabGroup renders the tabs and their content from TabTracker's labels and markup.

tapestry-stitch demonstrates a different technique: to avoid rendering tabs that may never be chosen, tapestry-stitch's TabGroup defers the rendering of each tab until the tab is chosen.

References: Bootstrap Tabs, t:body, Environment.

Home


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- We need a doctype to allow us to use special characters like &nbsp; 
     We use a "strict" DTD to make IE follow the alignment rules. -->
     
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
<body class="container">
    <h3>Generating Tabs</h3>
    
    <p>Creating a component to generate tabs based on what's in its body is a bit trickier. Here is how JumpStart's <code>TabGroup</code> component does it...</p> 
    
    <ol>
        <li>In beginRender(), TabGroup does not yet know what is in its body, so it pushes a <code>TabTracker</code> into the Environment.</li>
        <li>As the body renders, each <code>Tab</code> (inside <code>SourceCodeTab</code>) records a label and markup in TabTracker <em>instead of in the DOM</em>.</li> 
        <li>In afterRenderBody(...), TabGroup pops the TabTracker from the Environment</li>
        <li>TabGroup renders the tabs and their content from TabTracker's labels and markup.</li>
    </ol>
    
    <p><a href="http://tapestry-stitch.uklance.cloudbees.net/tabgroupdemo">tapestry-stitch</a> demonstrates a 
    different technique: to avoid rendering tabs that may never be chosen, tapestry-stitch's TabGroup defers the rendering of each tab 
    until the tab is chosen.</p>
    
    References: 
    <a href="http://getbootstrap.com/components/#nav-tabs">Bootstrap Tabs</a>, 
    <a href="http://tapestry.apache.org/component-templates.html#ComponentTemplates-The%3Ct%3Abody%3EElement">t:body</a>, 
    <a href="http://tapestry.apache.org/5.4/apidocs/org/apache/tapestry5/services/Environment.html">Environment</a>.<br/><br/> 

    <t:pagelink page="Index">Home</t:pagelink><br/><br/>
    
    <t:tabgroup>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/navigation/GeneratingTabs.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/pages/examples/navigation/GeneratingTabs.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/TabGroup.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/TabGroup.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/models/TabTracker.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/SourceCodeTab.tml"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/SourceCodeTab.java"/>
        <t:sourcecodetab src="/web/src/main/java/jumpstart/web/components/Tab.java"/>
    </t:tabgroup>
</body>
</html>


package jumpstart.web.pages.examples.navigation;

public class GeneratingTabs {
}


<!-- Based on Tapestry Stitch's TabGroup (http://tapestry-stitch.uklance.cloudbees.net) 
    and Java Magic's TabPanel (http://tawus.wordpress.com/2011/07/09/a-tab-panel-for-tapestry) . -->

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" xmlns:p="tapestry:parameter">
<t:content>
    <noscript>
        <div class="alert alert-warning">${message:javascript_required_for_tabs}</div>
    </noscript>

    <!-- We depend on the body not rendering to the DOM, and instead rendering into TabTracker! -->
    <t:body />

    <ul class="nav nav-tabs">
        <t:loop source="tabIds" value="tabId" index="tabNum">
            <li class="${active}">
                <a href="#${tabId}" data-toggle="tab">${tabLabel}</a>
            </li>
        </t:loop>
    </ul>

    <div class="tab-content">
        <t:loop source="tabIds" value="tabId" index="tabNum">
            <div id="${tabId}" class="tab-pane ${active}">
                <!-- Get the rendered markup that was put in TabTracker, above, in t:body. -->
                <t:delegate to="tabMarkup" />
            </div>
        </t:loop>
    </div>

</t:content>
</html>


// Based on Tapestry Stitch's TabGroup (http://tapestry-stitch.uklance.cloudbees.net) 
// and Java Magic's TabPanel (http://tawus.wordpress.com/2011/07/09/a-tab-panel-for-tapestry) .

package jumpstart.web.components;

import java.util.ArrayList;
import java.util.List;

import javax.inject.Inject;

import jumpstart.web.models.TabTracker;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.runtime.RenderCommand;
import org.apache.tapestry5.runtime.RenderQueue;
import org.apache.tapestry5.services.Environment;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;

public class TabGroup {

    // Screen fields

    @Property
    private List<String> tabIds;

    @Property
    private String tabId;

    @Property
    private int tabNum;

    // Work fields

    private List<String> tabLabels;
    private List<String> tabMarkups;

    // Generally useful bits and pieces

    @Inject
    private Environment environment;

    @Inject
    private JavaScriptSupport javaScriptSupport;

    @Inject
    private ComponentResources componentResources;

    // The code

    /**
     * The tricky part is that we can't render the navbar before we've rendered the body because we don't know how many
     * elements are in the body nor what labels they would like. We solve this by making a TabTracker available to the
     * body. Each Tab in the body will put its label and markup in TabTracker instead of rendering it. Afterwards, in
     * our afterRenderBody(), we will read TabTracker and render the tabs and tab content.
     */
    void beginRender() {
        environment.push(TabTracker.class, new TabTracker());
    }

    /**
     * By the time this method is called, we expect each Tab in the body of this component to have recorded a label and
     * markup in TabTracker instead of rendering it. Using what's in TabTracker we get ready to render tabs and tab
     * content.
     */
    void afterRenderBody(MarkupWriter markupWriter) {
        TabTracker tabTracker = environment.pop(TabTracker.class);

        tabLabels = tabTracker.getLabels();
        tabMarkups = tabTracker.getMarkups();

        // Invent unique ids for each tab.

        tabIds = new ArrayList<>();

        for (int i = 0; i < tabLabels.size(); i++) {
            String id = javaScriptSupport.allocateClientId(componentResources);
            tabIds.add(id);
        }
    }

    void afterRender() {
        // We depend on http://getbootstrap.com/javascript/#tabs . We use its Markup technique.
        javaScriptSupport.require("bootstrap/tab");
    }

    public String getActive() {
        return tabNum == 0 ? "active" : "";
    }

    public String getTabLabel() {
        return tabLabels.get(tabNum);
    }

    public RenderCommand getTabMarkup() {
        return new RenderCommand() {
            public void render(MarkupWriter writer, RenderQueue queue) {
                writer.writeRaw(tabMarkups.get(tabNum));
            }
        };
    }
}


// Based on Tapestry Stitch's TabGroup (http://tapestry-stitch.uklance.cloudbees.net) 
// and Java Magic's TabPanel (http://tawus.wordpress.com/2011/07/09/a-tab-panel-for-tapestry) .

package jumpstart.web.models;

import java.util.ArrayList;
import java.util.List;

public class TabTracker {

    private List<String> labels = new ArrayList<>();
    private List<String> markups = new ArrayList<>();

    public void addTab(String label, String markup) {
        labels.add(label);
        markups.add(markup);
    }

    public List<String> getLabels() {
        return labels;
    }

    public List<String> getMarkups() {
        return markups;
    }
}


<!-- Based on Tapestry Stitch's TabGroup (http://tapestry-stitch.uklance.cloudbees.net) 
    and Java Magic's TabPanel (http://tawus.wordpress.com/2011/07/09/a-tab-panel-for-tapestry). -->

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" xmlns:p="tapestry:parameter">
<t:content>
    <t:tab label="prop:name">
        <t:sourcecodedisplay src="prop:src" />
    </t:tab>
</t:content>
</html>


// Based on Tapestry Stitch's TabGroup (http://tapestry-stitch.uklance.cloudbees.net) 
// and Java Magic's TabPanel (http://tawus.wordpress.com/2011/07/09/a-tab-panel-for-tapestry).

package jumpstart.web.components;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.annotations.Property;

public class SourceCodeTab {

    @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL)
    @Property
    private String src;

    public String getName() {
        return extractSimpleName(src);
    }

    private String extractSimpleName(String path) {
        String simpleName = path;

        int i = path.lastIndexOf("/");
        simpleName = path.substring(i + 1);

        return simpleName;
    }
}


// Based on Tapestry Stitch's TabGroup (http://tapestry-stitch.uklance.cloudbees.net) 
// and Java Magic's TabPanel (http://tawus.wordpress.com/2011/07/09/a-tab-panel-for-tapestry).

package jumpstart.web.components;

import jumpstart.web.models.TabTracker;

import org.apache.tapestry5.BindingConstants;
import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Environmental;
import org.apache.tapestry5.annotations.Parameter;
import org.apache.tapestry5.dom.Element;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.services.Request;

public class Tab {

    @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL, allowNull = false)
    private String label;

    @Inject
    private ComponentResources componentResources;

    @Environmental
    private TabTracker tabTracker;

    @Inject
    private Request request;

    void beforeRenderBody(MarkupWriter writer) {

        // Make a container for the tab body.

        writer.element("div");
    }

    void afterRenderBody(MarkupWriter writer) {

        // End the container and record the label the body's markup in the TabTracker.

        Element bodyContainer = writer.getElement();
        writer.end();
        tabTracker.addTab(label, bodyContainer.getChildMarkup());

        // Remove the container (and therefore the body's markup) from the DOM. TabGroup will render the markup later at
        // its leisure.

        bodyContainer.remove();
    }
}