Tab panels are used so often in component based web design that many frameworks provide an out-of-box implementation. ChenilleKit has one for Tapestry. Let us try another one.
The tab panel will require two components.
- A TabPanel component to manage the tabs and tab links.
- A Tab component representing a tab
Creating this component is more fun as it needs a two-way communication between parent and child component. TabPanel
(parent component) has to get title and disable status from the Tab
component(child) while the Tab
component has to check when to render itself depending on the selected tab in the TabPanel
. For parent to child communication we will use the Environment
service and for child to parent communication we will have use ComponentResources
.
The Environment
service is a stack based service where in we can push an object onto the stack at any component rendering phase and later pop out the object when not required. The object is available to all the phases between push and pop calls. This stack based design is apt for communication because of the way the component rendering works. For two components A and B where A contains B, rendering occurs in this fashion
A.setupRender() -> A.beginRender() -> A.beforeRenderTemplate()->A.beforeRenderBody()->
B.setupRender() -> B.beginRender() -> B.beforeRenderTemplate()->B.beforeRenderBody()->
B.afterRenderBody()->B.afterRenderTemplate()->B.afterRender()->B.cleanupRender()
A.afterRenderBody()->A.afterRenderTemplate()->A.afterRender()->A.cleanupRender()
All the phases of child component occur within the phases of parent component. So, we can easily push an object in a phase before the child component is rendered and pop it out in a later phase. For a full description and understanding read this
To get the child components, we need their component ids. The information is passed in the tabs’ parameter of the TabPanel
. The component id can be used to get the actual component by using ComponentResources.getContainerResources().getEmbeddedComponent()
.
The source code for TabPanel
shows the implementation of these concepts
@Import(stylesheet = "tab-panel.css") public class TabPanel implements ClientElement { @Parameter(value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL, allowNull = false) private String clientId; @Parameter(defaultPrefix = BindingConstants.LITERAL) private String active; @SuppressWarnings("unused") @Parameter(defaultPrefix = BindingConstants.LITERAL) private String zone; @Parameter(defaultPrefix = BindingConstants.LITERAL) private String tabs; private String assignedClientId; @Property private String currentTabId; @SuppressWarnings("unused") @Property private int index; @Inject private JavaScriptSupport javaScriptSupport; @Inject private ComponentResources resources; @Inject private Environment environment; @Inject private Request request; private String [] tabsCache; public TabPanel() { } //Testing purpose TabPanel(String tabs, String active, String currentTabId, JavaScriptSupport javaScriptSupport, ComponentResources resources, Environment environment) { this.tabs = tabs; this.active = active; this.currentTabId = currentTabId; this.javaScriptSupport = javaScriptSupport; this.resources = resources; this.environment = environment; } void setupRender() { if(tabs == null || getTabs().length == 0) { throw new IllegalArgumentException("You must specify atleast one tab"); } if(active == null) { active = getTabs()[0]; } assignedClientId = javaScriptSupport.allocateClientId(clientId); } void beginRender() { environment.push(TabContext.class, new TabContext() { public boolean isActiveTab(String tabId) { return active != null && active.equals(tabId); } }); } void afterRender() { environment.pop(TabContext.class); } public String getClientId() { return assignedClientId; } Object onSelectTab(String selected) { active = selected; CaptureResultCallback<Object> callback = new CaptureResultCallback<Object>(); boolean handled = resources.triggerEvent(EventConstants.SELECTED, new Object[]{selected}, callback); if(request.isXHR() & !handled) { throw new TapestryException(String.format("Event %s not handled", EventConstants.SELECTED), null); } return callback.getResult(); } public String getCssClass() { return isActiveTab() ? "t-tab-active" : "t-tab-default"; } public boolean isActiveTab() { return currentTabId.equals(active); } public String getActive() { return active; } public Tab getCurrentTab() { return getTab(currentTabId); } private Tab getTab(String tabId) { return (Tab) resources.getContainerResources().getEmbeddedComponent(tabId); } public String [] getTabs() { if(tabsCache == null) { tabsCache = TapestryInternalUtils.splitAtCommas(tabs); } return tabsCache; } }
<t:container xmlns:t='http://tapestry.apache.org/schema/tapestry_5_1_0.xsd'> <div class='t-tab-panel'> <ul class='t-tab-links'> <t:loop t:source='tabs' t:value='currentTabId' t:index='index'> <t:unless test='currentTab.disabled'> <li class='${cssClass}'> <a t:zone='inherit:zone' t:type='eventlink' t:context='currentTabId' t:event='selectTab'>${currentTab.title}</a> </li> </t:unless> </t:loop> </ul> <div class='t-tab-content'> <t:body/> </div> </div> </t:container>
We push TabContext
in beginRender
phase and pop it out in afterRender
, which makes it available to the Tab
for its entire component rendering phases. One important point to notice it that the object is pushed and poped in rendering phases and is not available in action phases which means you can’t use this object in event handlers.
TabContext
is a simple interface
public interface TabContext { boolean isActiveTab(String tabId); }
The Tab
component uses the TabContext
to verify if it should render its contents. In case the tab is not active or disabled, rendering is skipped by returning false in beginRender
phase.
public class Tab { @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL, allowNull = false) private String title; @Parameter(value = "false", defaultPrefix = BindingConstants.LITERAL, allowNull = false) private boolean disabled; @Environmental private TabContext tabContext; @Inject private ComponentResources resources; boolean beginRender() { return isActiveAndEnabled(); } private boolean isActiveAndEnabled() { return tabContext.isActiveTab(resources.getId()) && !disabled; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public boolean getDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } }
<t:container xmlns:t='http://tapestry.apache.org/schema/tapestry_5_1_0.xsd'> <t:body/> </t:container>
Usage
The typical usage will be
public class TabPanelDemo { @Property private String active; void onActivate(String active) { this.active = active; } String onPassivate() { return active; } }
<html xmlns:t='http://tapestry.apache.org/schema/tapestry_5_1_0.xsd'> <body> <div t:type='tawus/tabpanel' t:tabs='tabA, tabB,tabC' t:active='prop:active' t:id='outer'> <div t:type='tawus/tab' title='Tab A' t:id='tabA'>Content of Tab A</div> <div t:type='tawus/tab' title='Tab B' t:id='tabB'>Content of Tab B</div> </div> </div> </body> </html>
Nice! It should be possible however to skip the tabs parameter in tabpanel and make tab components discoverable.
Couldn’t find a way to do that. Any pointers ?
What is the Maven artifact to put in the dependencies?
Hi, Is it possible to implement this without loosing data that was entered in to one tab while moving to other tabs
No it won’t be. I would use a javascript based tab-panel along with different zone updates for each tab-pane
I have the same question as Simon “Is it possible to implement this without loosing data that was entered in to one tab while moving to other tabs’. I used @persist but it does not work.