Thursday, March 3, 2011

Styling individual (and nested) tabs using GWT

I've read several forum entries trying to explain how to style GWT TabLayoutPanels, and there seems to be several ways to do it. The three most common ways are to:

1) Edit the standard.css-file and update the .gwt-TabLayoutPanel, .gwt-TabLayoutPanelTabs, .gwt-TabLayoutPanelTab, .gwt-TabLayoutPanelTabInner and .gwt-TabLayoutPanelContent.
2) Override the styles by putting your own css-file as part of html hostpage.
3) Override the styles in the ui-binder file using the @external keyword.

None of them really fit my needs... I want to be able to style indidividual tabs, including nesting tabs within tabs (with different styling). The methods above changes the globally set stylenames, and applies to all tabs being used in you application.

What I'm looking for is to style the tabs using defined styles in the ui-binder file. This way, the styles will be obfuscated and possible to apply within it's own namespace - hence only applied to individual tabs using that namespace.

Let's see an example of how I have managed to set this up. (I'm sure there are other/smarter/better ways, but looking at the forums - people aren't sharing the secrets...)

First of all, let's create the ui-binder file:

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
 xmlns:g="urn:import:com.google.gwt.user.client.ui">

 <ui:style type="com.technowobble.client.NestedTabWidget.Style"> 
  
  .my-TabLayoutPanel {
   background-color: red !important; 
  }
  
  .myTabLayoutPanelTab {
   background-color: #DCDCDC !important;
   color: gray !important;
   -webkit-border-top-left-radius: 3px;
   -webkit-border-top-right-radius: 3px;
   -moz-border-radius-topleft: 3px;
   -moz-border-radius-topright: 3px;
   border-top-left-radius: 3px;
   border-top-right-radius: 3px;
   margin-left: 11px !important;   
  }
  
  .myTabLayoutPanelTabSelected {
   background-color: #2E8B57 !important;
   color: black !important;
  }
    
  .myTabLayoutPanelTabsContainer {
   background-color: orange;
   -moz-border-radius-topleft: 10px;
   -webkit-border-top-left-radius: 10px;
   -moz-border-radius-bottomleft: 10px;
   -webkit-border-bottom-left-radius: 10px;
   -moz-border-radius-topright: 10px;
   -webkit-border-top-right-radius: 10px;
   -moz-border-radius-bottomright: 10px;
   -webkit-border-bottom-right-radius: 10px;
  } 
  
  .myTabLayoutPanelContent {
   border-color: #2E8B57 !important;
  }   
  
  .myNestedTabLayoutPanel {
   background-color: lime !important; 
  }
  
  .myNestedTabLayoutPanelTab {
   background-color: black !important;
   color: gray !important;
   -webkit-border-top-left-radius: 3px;
   -webkit-border-top-right-radius: 3px;
   -moz-border-radius-topleft: 3px;
   -moz-border-radius-topright: 3px;
   border-top-left-radius: 3px;
   border-top-right-radius: 3px;
   margin-left: 11px !important;
  }
 
  .myNestedTabLayoutPanelTabSelected {
   background-color: #7B68EE !important;
   color: black !important;
  }
  
  .myNestedTabLayoutPanelContent {
   border-color: #7B68EE !important;
   background-color: fuchsia !important;
   color: white;
  }  
 </ui:style>

 <g:TabLayoutPanel ui:field="tabLayoutPanel" barHeight="2" barUnit="EM" width="400px" height="150px" addStyleNames="{style.my-TabLayoutPanel}">
  <g:tab>
   <g:header>Tab 1</g:header>
   <g:TabLayoutPanel barHeight="2" barUnit="EM">
    <g:tab>
     <g:header>Nested tab 1.1</g:header>
     <g:HTML wordWrap="true">Nested tab 1.1 content</g:HTML>
    </g:tab>
    <g:tab>
     <g:header>Nested tab 1.2</g:header>
     <g:HTML wordWrap="true">Nested tab 1.2 content</g:HTML>
    </g:tab>
   </g:TabLayoutPanel>
  </g:tab>
  <g:tab>
   <g:header>Tab 2</g:header>
   <g:TabLayoutPanel barHeight="2" barUnit="EM">
    <g:tab>
     <g:header>Nested tab 2.1</g:header>
     <g:HTML wordWrap="true">Nested tab 2.1 content</g:HTML>
    </g:tab>
    <g:tab>
     <g:header>Nested tab 2.2</g:header>
     <g:HTML wordWrap="true">Nested tab 2.2 content</g:HTML>
    </g:tab>
    <g:tab>
     <g:header>Nested tab 2.3</g:header>
     <g:HTML wordWrap="true">Nested tab 2.3 content</g:HTML>
    </g:tab>    
   </g:TabLayoutPanel>
  </g:tab>
 </g:TabLayoutPanel>
</ui:UiBinder> 

This file defines a tab panel with two tabs, each of which have a separate tab panel as it's content. The specified style tags pretty much mimics the "standard" style names for a TabLayoutPanel and I've named them thereafter. Remember, they could be named whatever as they will be obfuscated anyway.

The only style that is actually applied directly in the ui-binder file is the .my-TabLayoutPanel, which could just as well have had applied at run-time instead.

Now, let's create the corresponding Java-class, which will style the defined tabs using the above styles:

package com.technowobble.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.TabLayoutPanel;
import com.google.gwt.user.client.ui.Widget;

public class NestedTabWidget extends Composite {

 private static NestedTabWidgetUiBinder uiBinder = GWT
   .create(NestedTabWidgetUiBinder.class);

 interface NestedTabWidgetUiBinder extends UiBinder<Widget, NestedTabWidget> {
 }
 
 interface Style extends CssResource {
  // Tab container (containing the .gwt-TabLayoutPanelTabs styling
  String myTabLayoutPanelTabsContainer();
  // General tab styling
  String myTabLayoutPanelTab();
  // Specific styling for selected tab
  String myTabLayoutPanelTabSelected();
  // Applied to tab content
  String myTabLayoutPanelContent();
  // Applied to entire nested tab panel
  String myNestedTabLayoutPanel();
  // General inner tab styling
  String myNestedTabLayoutPanelTab();
  // Specific styling for selected inner tab
  String myNestedTabLayoutPanelTabSelected();
  // Applied to inner tab content
  String myNestedTabLayoutPanelContent();
 }
 
 @UiField 
 Style style;
 
 @UiField
 TabLayoutPanel tabLayoutPanel;

 public NestedTabWidget() {
  initWidget(uiBinder.createAndBindUi(this));
  
  // style the tab container (gray bar)
  ((Element) tabLayoutPanel.getElement().getChild(1)).addClassName(style.myTabLayoutPanelTabsContainer());
  // style the tabs
  styleTabPanel(tabLayoutPanel, style.myTabLayoutPanelTab(), style.myTabLayoutPanelTabSelected(), style.myTabLayoutPanelContent());
  
  // style all inner tabs (in uibinder)
  for (int i = 0; i < tabLayoutPanel.getWidgetCount(); i++) {
   Widget widget = tabLayoutPanel.getWidget(i);
   
   // add content style no matter what the content is
   widget.addStyleName(style.myTabLayoutPanelContent());
   
   if (widget instanceof TabLayoutPanel) {
    final TabLayoutPanel nestedTabLayoutPanel = (TabLayoutPanel) widget;
    // style inner tabLayoutPanel  
    nestedTabLayoutPanel.addStyleName(style.myNestedTabLayoutPanel());
    styleTabPanel(nestedTabLayoutPanel, style.myNestedTabLayoutPanelTab(), style.myNestedTabLayoutPanelTabSelected(), style.myNestedTabLayoutPanelContent());

    // re-style on selects
    nestedTabLayoutPanel.addSelectionHandler(new SelectionHandler<Integer>() {     
     @Override
     public void onSelection(SelectionEvent<Integer> event) {
      styleTabPanel(nestedTabLayoutPanel, style.myNestedTabLayoutPanelTab(), style.myNestedTabLayoutPanelTabSelected(), style.myNestedTabLayoutPanelContent());      
     }
    });       
   }
  }
 }
 
 @UiHandler("tabLayoutPanel")
 public void onTabSelect(SelectionEvent<Integer> event) {
  styleTabPanel(tabLayoutPanel, style.myTabLayoutPanelTab(), style.myTabLayoutPanelTabSelected(), style.myTabLayoutPanelContent());
 } 
  
 private void styleTabPanel(TabLayoutPanel tabPanel, String style, String styleSeleced, String styleContent) {
  for (int i = 0; i < tabPanel.getWidgetCount(); i++) {
   Widget tab = tabPanel.getTabWidget(i);
   
   // style the actual tab
   tab.getParent().addStyleName(style);
   if (i == tabPanel.getSelectedIndex()) {
    tab.getParent().addStyleName(styleSeleced);
   } else {
    tab.getParent().removeStyleName(styleSeleced);
   }
   
   // style the added content
   Widget content = tabPanel.getWidget(i);
   content.addStyleName(styleContent);
  } 
 }
}

You can see what the different styles are used for in the comments (according to the JavaDoc of the TabLayoutPanel) - the only interesting thing to note is that I've added styling to the div-tag containing the .gwt-TabLayoutPanelTabs div, in order to style the background behind the tabs.

When initializing the widget I style all the tabs once, but as I can't use addStyleDependantName() to style select/unselect I also have to apply these styles on every select/unselect. This is applied to both the outer TabLayoutPanels, as the inner TabLayoutPanels. The reason the addStyleDependantName() can't be used is because the names are obfuscated, and adding a "-select" to it doesn't match the name of the non-obfuscated corresponding selected style in the ui-binder file. There are surely a better way of doing this, and I'd be happy to hear if you have any suggestions here...

I haven't put up any running example of this, but you can download the source code as an Eclipse-project here and try for yourself. I apologize for the color-schema I've used in the example - looks like a 5-year old gone crazy, but it fills the purpose of showing where each style gets applied...

Have fun!

2 comments:

  1. Alternatively you could "mark" tabMenu with your class, and then style it like this:

    <ui:style>
    @external gwt-TabLayoutPanelTabs, gwt-TabLayoutPanelTab, gwt-TabLayoutPanelTab-selected, gwt-TabLayoutPanelTabInner;

    .mainTabMenu > * > .gwt-TabLayoutPanelTabs {
    }
    .mainTabMenu > * > * > .gwt-TabLayoutPanelTab {
    }
    .mainTabMenu > * > * > .gwt-TabLayoutPanelTab-selected {
    }
    .mainTabMenu > * > * > * > .gwt-TabLayoutPanelTabInner {
    }
    </ui:style>

    <g:TabLayoutPanel styleName="{style.mainTabMenu}" .../>

    This would allow you to have inner tabLayoutPanel marked with different class, e.g. "innerTabManu" and then you would use similar rules which apply only to that tabMenu.

    Of course, this relies on the tabLayoutPanel implementation - how they nest their layers, so this potentially could break if they changed the widget dramatically.

    Note for non-nested tabMenus, if you have them as siblings on the page, you do not need to manually specify levels of nesting "myMenu > * > gwt-XXXX", you could just do ".myMenu .gwt-XXX".

    ReplyDelete