A Tab-panel for Tapestry

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>

Tagged: , ,

6 thoughts on “A Tab-panel for Tapestry

  1. Andy July 11, 2011 at 2:33 AM Reply

    Nice! It should be possible however to skip the tabs parameter in tabpanel and make tab components discoverable.

    • tawus July 13, 2011 at 9:47 PM Reply

      Couldn’t find a way to do that. Any pointers ?

  2. dongmei July 16, 2011 at 1:13 AM Reply

    What is the Maven artifact to put in the dependencies?

  3. Simon November 4, 2011 at 4:26 AM Reply

    Hi, Is it possible to implement this without loosing data that was entered in to one tab while moving to other tabs

    • tawus November 17, 2012 at 7:01 AM Reply

      No it won’t be. I would use a javascript based tab-panel along with different zone updates for each tab-pane

  4. Marion November 13, 2012 at 10:06 PM Reply

    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.

Leave a reply to tawus Cancel reply