Skip to main content

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!

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

Post a Comment

Popular posts from this blog

GWT and Spring Security

Update! - Based on the post below, and my other post regarding Spring Security and OpenID, I have added Open-ID support to the sample application below. For those interested, here's the write-up of changes. I've spent quite some time digging into ways of integrating GWT and Spring Security. It all started by reading the following post in the GWT Forum - Best practices/ideas for GWT with Spring Security (or equivalent) , and then checking out this blog - GWT and Spring Security . To make matters worse, I started reading Security for GWT Applications and specifically about the "Cross-Site Request Forging"-attacks. Now, what could I do about it? Well, starting by setting up my own project (Maven-based) with all updated dependencies (GWT 2.0.3 etc) and started reading the Spring Security Reference Documentation (puh!). Instead of See Wah Cheng's approach of implementing a custom authentication service, I decided to rely on standard namespace configuration

Loading Google Maps API asynchronously with RequireJS

With Single Page Web Applications becoming more and more popular, I decided to understand the concepts of various Javascript frameworks a little better. There are literally hundreds of them, but I decided to start with a really nice tutorial written by Alex Young. Cornerstones in this tutorial are BackboneJS, Underscore, Bootstrap and RequireJS. After been through the tutorial I decided to roll my own project based on the same setup. I wanted to use Google Maps for this, and searched for a way to load the API using RequireJS. Turned out that there are a few different approaches, but the most common seems to be to use the async-plugin created by Miller Medeiros. Jason Wyatt has another interesting solution which caught my attention. Being new to all this, I really didn't feel like start involving plug-ins from remote repositories. It might be the most natural thing to do, but one step at a time is more my melody. Jason's solution had some drawbacks mentioned in the comm

Google Apps Script and ES Modules

Currently, Google Apps Script does not support ES modules - and any usage of export/import will fail. One way of handling this is to use rollup.js to bundle your project into one single JavaScript file. The trick here is to make sure not to export any functions in your entry point code, e.g. index.ts , and to prevent any generation of export statement in the final bundle (see the custom rollup plugin in the rollup.config.js below). import { babel } from "@rollup/plugin-babel"; import { nodeResolve } from "@rollup/plugin-node-resolve"; const extensions = [".ts", ".js"]; const preventThreeShakingPlugin = () => { return { name: 'no-threeshaking', resolveId(id, importer) { if (!importer) { // let's not theeshake entry points, as we're not exporting anything in Apps Script files return {id, moduleSideEffects: "no-treeshake" } } return null; }