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...
TabTracker
into the Environment.Tab
(inside SourceCodeTab
) records a label and markup in TabTracker instead of in the DOM.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.
<!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
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();
}
}