J2EE developer with over 7 years of experience in designing and implementing enterprise j2ee solutions based on open source technologies like Tapestry, Hibernate, Spring. Current interests include Tapestry, Plastic, Spock, Scala. Taha is a DZone MVB and is not an employee of DZone and has posted 40 posts at DZone. You can read more from them at their website. View Full User Profile

A Tab-panel for Tapestry

07.11.2011
| 3065 views |
  • submit to reddit

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>

From http://tawus.wordpress.com/2011/07/09/a-tab-panel-for-tapestry/

Published at DZone with permission of Taha Siddiqi, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)