JSF composite component example

13 March 2014
By Gonçalo Marques
In this article we will implement a custom composite component in JSF.

Introduction

JSF provides all the required infrastructure for creating composite components. These components may be defined using only XML, only Java, or a combination of both, depending on the complexity of the desired funcionality.

Composite components may also be packaged in a library file (JAR) and be reused across multiple application if they are properly designed.

In this article we will implement a toggle panel (or tab view) completely from scratch. The final result will look like the following:

Resulting tab view
Tab view JSF composite component

It's the typical tab view, where the user clicks on a desired tab in order to see the respective content displayed (inside a predefined content area). The tab view look and feel it's not sophisticated, but that is not the focus of this article. We will focus solely on the component proper implementation.

The tab view we will implement may be defined in a couple of ways:

Alternative 1 - fixed tab definition

<bl:togglePanel activeIndex="#{testBean.activeIndex}" ajaxEnabled="true">

  <bl:togglePanelSection linkText="Section 1">
    <f:facet name="header">
      <h:outputText value="Header - Section 1" />
    </f:facet>
    <h:outputText value="This is the content from section one" />
  </bl:togglePanelSection>
  
  <bl:togglePanelSection linkText="Section 2">
    <f:facet name="header">
      <h:outputText value="Header - Section 2" />
    </f:facet>
    <h:outputText value="This is the content from section two" />
  </bl:togglePanelSection>
  
</bl:togglePanel>

The first possible configuration consists in a fixed tab definition, where the available tabs are individually defined inside the container. The tabs are represented by togglePanelSection and the tab view itself - the tab container - is represented by togglePanel.

The tab content may be defined with any JSF controls we need. In this case we are showing a simple output text as the tab content for all tabs. We are also defining a header for each tab (again, consisting only in an output text for all tabs).

All of the available component values may be obviously bound to a managed bean.

Alternative 2 - dynamic tab definition

<bl:togglePanel activeIndex="#{testBean.activeIndex}" ajaxEnabled="true">

  <ui:repeat var="item" value="#{testBean.items}">
  
    <bl:togglePanelSection linkText="#{item.linkText}">
      <f:facet name="header">
        <h:outputText value="#{item.headerText}" />
      </f:facet>
      <h:outputText value="#{item.contentText}" />
    </bl:togglePanelSection>
    
  </ui:repeat>
  
</bl:togglePanel>

The second available alternative is to define the tab view with a dynamic number of tabs. We define a tab template and use a repeat in order to iterate over a list and render as much tabs as available items in the list, as we usually do with any other JSF component or control.

Finally, the tab view has an activeIndex attribute, which defines the index of the tab that should be opened by default (if we don't define the active index it will default to 1). We also have an ajaxEnabled attribute that defines if the tab changing operation is performed over ajax (default is false).

This tutorial considers the following environment:

  1. Ubuntu 12.04
  2. JDK 1.7.0.21
  3. Glassfish 4.0 (with Mojarra 2.2.3 installed)

Component definition

We start by defining the component XML layout. First we need the definition for the individual tabs, which is the following:

togglePanelSection.xhtml

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:cc="http://java.sun.com/jsf/composite"
  xmlns:h="http://java.sun.com/jsf/html">
<h:head>
  <title>This content will not be displayed</title>
</h:head>
<h:body>
  <cc:interface
    componentType="com.byteslounge.component.TogglePanelSection">
    <cc:facet name="header" />
  </cc:interface>

  <cc:implementation>
    <h:panelGroup rendered="#{cc.active}" layout="block" 
        styleClass="tp-section">
      <h:panelGroup layout="block" styleClass="tp-header">
        <cc:renderFacet name="header" />
      </h:panelGroup>
      <h:panelGroup layout="block">
        <cc:insertChildren />
      </h:panelGroup>
    </h:panelGroup>
  </cc:implementation>

</h:body>
</html>

And now the tab view itself (the container):

togglePanel.xhtml

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:cc="http://java.sun.com/jsf/composite"
  xmlns:h="http://java.sun.com/jsf/html"
  xmlns:ui="http://java.sun.com/jsf/facelets"
  xmlns:f="http://java.sun.com/jsf/core">
<h:head>
  <title>This content will not be displayed</title>
</h:head>
<h:body>

  <cc:interface componentType="com.byteslounge.component.TogglePanel">
    <cc:attribute name="activeIndex" type="java.lang.Integer" />
    <cc:attribute name="ajaxEnabled" type="java.lang.Boolean" />
  </cc:interface>

  <cc:implementation>
    <h:form id="#{cc.formId}">
      <h:panelGroup id="#{cc.buttonsPanelId}" layout="block" 
         styleClass="tp-links">
      </h:panelGroup>
    </h:form>
    <h:panelGroup id="#{cc.tabsPanelId}" layout="block">
      <cc:insertChildren />
    </h:panelGroup>
  </cc:implementation>

</h:body>
</html>

Both definitions are very straight forward. One should note that we have a form in the tab view definition (togglePanel.xhtml) that will be responsible for holding the tab changing command links, and consequently handle these command link actions and refresh the page in order to display the selected tab (or refresh only the tab view section if Ajax is enabled).

The individual tab definition (togglePanelSection.xhtml) contains a panelGroup that is conditionally rendered (depends on #{cc.active}). This means that only the currently active tab will be actually rendered in the response.

Each component definition has a componentType attribute that contains the Java class which will be used to represent the component implementation. This means that the cc that is present in the component EL expressions will refer to the component Java instance, so the expressions must match against properties in the component Java class.

Component Java classes

Now we define the Java classes for each component (the individual tab and the container). This time we start by the tab container definition:

TogglePanel.java

package com.byteslounge.component;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.el.ELContext;
import javax.el.ValueExpression;
import javax.faces.application.ResourceDependency;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.component.behavior.AjaxBehavior;
import javax.faces.component.html.HtmlCommandLink;
import javax.faces.component.html.HtmlOutputText;
import javax.faces.component.visit.VisitCallback;
import javax.faces.component.visit.VisitContext;
import javax.faces.component.visit.VisitResult;
import javax.faces.context.FacesContext;

@FacesComponent("com.byteslounge.component.TogglePanel")
@ResourceDependency(
  library = "bl", 
  name = "css/togglepanel.css", 
  target = "head"
)
public class TogglePanel extends UINamingContainer {

  private static final String FORM_ID = "tpForm";
  private static final String BUTTONS_PANEL_ID = "tpBtns";
  private static final String TABS_PANEL_ID = "tpTabs";

  enum PropertyKeys {
    activeIndex, ajaxEnabled, tabIndexMap
  }

  @Override
  public void encodeBegin(final FacesContext context) 
       throws IOException {

    final UIComponent buttonPanel = 
           findComponent(FORM_ID).findComponent(BUTTONS_PANEL_ID);
    final Map<String, Integer> tabIndexMap = getTabIndexMap();

    buttonPanel.getChildren().clear();
    tabIndexMap.clear();

    VisitCallback callback = new VisitCallback() {

      private int tabIndex = 1;

      @Override
      public VisitResult visit(VisitContext visitContext,
          UIComponent component) {

        if (component instanceof TogglePanelSection) {

          HtmlCommandLink button = (HtmlCommandLink) context
              .getApplication().createComponent(
                  HtmlCommandLink.COMPONENT_TYPE);

          tabIndexMap.put(component.getClientId(), tabIndex);

          if (getAjaxEnabled()) {
            button.setId("tbBtn" + tabIndex);
            AjaxBehavior ajax = new AjaxBehavior();
            List<String> executes = new LinkedList<String>();
            List<String> renders = new LinkedList<String>();
            executes.add("@form");
            ajax.setExecute(executes);
            renders.add("@form");
            renders.add(getSeparatorChar(context)
                + TogglePanel.this.findComponent(TABS_PANEL_ID)
                    .getClientId());
            ajax.setRender(renders);
            button.addClientBehavior(button.getDefaultEventName(),
                ajax);
          }

          button.setValue(((TogglePanelSection) component)
              .getLinkText());
          button.setActionExpression(context
              .getApplication()
              .getExpressionFactory()
              .createMethodExpression(context.getELContext(),
                  "#{cc.setActiveIndex(" + tabIndex + ")}",
                  String.class, new Class<?>[] {}));

          buttonPanel.getChildren().add(button);

          HtmlOutputText separator = (HtmlOutputText) context
              .getApplication().createComponent(
                  HtmlOutputText.COMPONENT_TYPE);
          separator.setValue("-");
          buttonPanel.getChildren().add(separator);

          tabIndex++;
          return VisitResult.REJECT;
        }
        return VisitResult.ACCEPT;
      }
    };

    this.visitTree(VisitContext.createVisitContext(context), callback);

    setTabIndexMap(tabIndexMap);

    // Remove last separator
    buttonPanel.getChildren()
        .remove(buttonPanel.getChildren().size() - 1);

    super.encodeBegin(context);
  }

  public int getActiveIndex() {
    return (Integer) getStateHelper().eval(PropertyKeys.activeIndex, 1);
  }

  public void setActiveIndex(int activeIndex) {
    getStateHelper().put(PropertyKeys.activeIndex, activeIndex);
    ELContext ctx = FacesContext.getCurrentInstance().getELContext();
    ValueExpression ve = 
        getValueExpression(PropertyKeys.activeIndex.name());
    if (ve != null) {
      ve.setValue(ctx, activeIndex);
    }
  }

  @SuppressWarnings("unchecked")
  public Map<String, Integer> getTabIndexMap() {
    return (Map<String, Integer>) getStateHelper().eval(
        PropertyKeys.tabIndexMap, new HashMap<>());
  }

  private void setTabIndexMap(Map<String, Integer> tabIndexMap) {
    getStateHelper().put(PropertyKeys.tabIndexMap, tabIndexMap);
  }

  public boolean getAjaxEnabled() {
    return (Boolean) getStateHelper().
        eval(PropertyKeys.ajaxEnabled, false);
  }

  public void setAjaxEnabled(boolean ajaxEnabled) {
    getStateHelper().put(PropertyKeys.ajaxEnabled, ajaxEnabled);
  }

  public String getButtonsPanelId() {
    return BUTTONS_PANEL_ID;
  }

  public String getFormId() {
    return FORM_ID;
  }

  public String getTabsPanelId() {
    return TABS_PANEL_ID;
  }

}


Things to note: Generally speaking, each JSF component has an associated state helper that may be used to, as the name states, keep the component state across the component life time or JSF request processing phases, even if the component life time spawns multiple requests (view scope + Ajax). We always use the state helper in order to store and retrieve the component attributes.

When we use a repeat in JSF, the container will include the tag content only a single time in the component tree. The repeat iteration and HTML generation is only done in the render response phase so we must visit the tree if we want to actually simulate the rendering behaviour in previous phases.

In our case we needed to visit the tree previously in order to generate the navigation links for the tab view, one for each tab. We also needed (during visit tree) to associate the generated tab ids to indexes by the means of a Map. This way we may later retrieve the Map in order to check which index corresponds a given tab.

The tab view also holds the current active tab index. As you may have already noticed, the command link action is bound to the setActiveIndex() method. This method will store the selected active index and also execute the EL expression associated with the activeIndex property. This last step will also update the activeIndex property if it was initally bound to a managed bean.

Last thing to note is the @ResourceDependency annotation. It represents the required resources for the component, in this case a stylesheet (css) file. This means that when the component is rendered in a JSF view, the container will include the declared css in the rendered page.

The remaining code should be almost self explanatory, so we will now proceed to the tab definition:

TogglePanelSection.java

package com.byteslounge.component;

import java.io.IOException;

import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;

@FacesComponent("com.byteslounge.component.TogglePanelSection")
public class TogglePanelSection extends UINamingContainer {

  enum PropertyKeys {
    active, linkText
  }

  @Override
  public void encodeBegin(FacesContext context) throws IOException {

    TogglePanel togglePanel = getContainingTogglePanel(this);
    int activeIndex = togglePanel.getActiveIndex();

    if (activeIndex == togglePanel.getTabIndexMap()
        .get(this.getClientId()).intValue()) {
      setActive(true);
    } else {
      setActive(false);
    }

    super.encodeBegin(context);
  }

  private TogglePanel getContainingTogglePanel(UIComponent component) {
    UIComponent parent = component.getParent();
    if (parent == null) {
      throw new RuntimeException(
          "TogglePanelSection was not used inside a TogglePanel!");
    }
    if (parent instanceof TogglePanel) {
      return (TogglePanel) parent;
    }
    return getContainingTogglePanel(parent);
  }

  public boolean getActive() {
    return (Boolean) getStateHelper().eval(PropertyKeys.active, true);
  }

  public void setActive(boolean active) {
    getStateHelper().put(PropertyKeys.active, active);
  }

  public String getLinkText() {
    return (String) getStateHelper().eval(PropertyKeys.linkText);
  }

  public void setLinkText(String linkText) {
    getStateHelper().put(PropertyKeys.linkText, linkText);
  }

}

Things to note in the tab definition: We fetch the first TogglePanel up in the component tree hierarchy as it is the container of the current tab being rendered. From here we fetch all the metadata we set previously in the container rendering - for example the active index and the current tab index - and decide to set the active attribute accordingly. This attribute will determine if the tab will be rendered or not in the reponse.

Another aspect we did not mentioned earlier is that we are doing our work in the encodeBegin method. This method is called before the component rendering. We could have written the component markup (HTML) directly to the output stream if we needed, but we will not cover that feature in this article.

One may use encodeBegin and encodeEnd in order to write markup before and after the component (and its childs) rendering, like the simple opening and close of a div element (usually we write much more complex markup than that). As we stated before we will not cover that feature in this article.

Note: We used Mojarra 2.2.3 because previous versions have a bug where the visit tree of a JSF repeat element will generate an extra - and wrong - iteration (see [#JAVASERVERFACES-2956] Too many iterations when visiting children of ui:repeat). If upgrading is not possible one may easily craft a workaround and revert the changes caused by the extra iteration, but we prefered to update JSF and keep the code clean.

The project folder structure is the following:

Project folder structure
Project folder structure

The article source code is available for download at the end of this page.

Download source code from this article

Related Articles

Comments

About the author
Gonçalo Marques is a Software Engineer with 8+ years of experience in software development and architecture design. During this period his main focus was delivering software solutions both in banking and telecommunications area. He created the Bytes Lounge website with one ultimate goal: share his knowledge with the software development community. His main area of expertise is Java and open source.

He is also the author of the WiFi File Browser Android application: