JSF Techniques –Adding Components Dynamically

© 2008, Software Engineering Solutions, Inc.

-         Damodar Chetty

-         Feb 24, 2009

 

Summary:

Given how important components are in the JSF universe, it is not surprising that JSF works very hard at making it easy for you to instantiate and use components. A lot of this happens as if by magic, without any developer intervention.

This “implicit creation by JSF” scenario is by far the most common usage, and simply requires the use of JSF templating tags like
<h:inputText id="userName" value="#{loginBean.username}"/>.

Here, JSF instantiates a new HtmlInputText component in the Render Response phase, and links the component’s value attribute to the userName property on the managed bean named loginBean.

The upsides with this approach are its convenience and simplicity – instantiation of components is done declaratively using the appropriate templating tags. This ensures that template files can be edited by non-Java developers.

However, the downside is the need to define, in advance, all of the components that the template will have at runtime. This is not quite as onerous as it might seem initially. Most pages have a finite set of known controls – e.g., the fields for a page that accepts credit card details. And, in the case that a particular field does not apply, it is easy to hide it from display using the rendered attribute; or disabling it using the disabled attribute.

Unfortunately, this solution does not work in the scenario where the fields that a page contains cannot be predefined at application startup, and instead depend on some dynamic set of conditions. Maybe because this list is defined by another software utility and stored in some canonical form as database records, and your JSF application must rehydrate those definitions into viable components.

In such cases, the templating mechanism of creating components is not quite adequate and we need heavier firepower.

This is where the “explicit creation by the developer” model shines. In this article we’ll look at how we might solve this problem, and while doing so, we’ll discuss how explicit component instantiation works.

 

The Sample Application:

The first page of this application presents a list of input fields with labels. As pointed out earlier, the fields on this page are generated dynamically. In fact the template XHTML file for this page consists of just one line that represents all the input fields that it contains:

<h:panelGrid id="mainGrid" binding="#{dynamicComponentBean.mainGrid}" columns="2" />

The Country field is a required field, and requires between 3 to 10 characters. Enter values in the other fields, leaving the Country field empty, and click Enter. You will see an appropriate validation message. Note that this validation message is defined by the application declaratively.

 

Repeat with one character and more than 10 character country names, and you’ll see custom validation messages for your application.

Finally, enter a Country name that meets these criteria, and click the submit button. The second page displays the values you entered for each of these fields.

Click the Back button to return to the first page, with the fields still retaining your original input.

 

 

Setting Up Your Project:

Let’s begin by examining the configuration files for this application. Please reference previous articles for the details on setting up your JSF application.

applicationContext.xml:

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">

   <bean id="dynamicComponentBean" scope="request"
       class="com.swengsol.managedbeans.DynamicComponentBean" />

</beans>

The only managed bean used is com.swengsol.managedbeans.DynamicComponentBean. This is created in request scope.

 

faces-config.xml

<?xml version='1.0' encoding='UTF-8'?>

<faces-config xmlns="http://java.sun.com/xml/ns/javaee"

 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

 xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
  http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"

 version="1.2">

  <application>

   <variable-resolver>
     org.springframework.web.jsf.DelegatingVariableResolver

   </variable-resolver>

   <message-bundle>com.messages</message-bundle>

  </application>

  <navigation-rule>

    <from-view-id>/DynamicPage1.xhtml</from-view-id>

   <navigation-case>

     <from-outcome>done</from-outcome>

     <to-view-id>/DynamicPage2.xhtml</to-view-id>

   </navigation-case>

  </navigation-rule>

  <navigation-rule>

    <from-view-id>/DynamicPage2.xhtml</from-view-id>

   <navigation-case>

     <from-outcome>back</from-outcome>

     <to-view-id>/DynamicPage1.xhtml</to-view-id>

   </navigation-case>

  </navigation-rule>

</faces-config>

No surprises here either. The navigation for our example is pretty rudimentary – after all we only have 2 pages.

 

messages.properties

javax.faces.validator.LengthValidator.MINIMUM={1}: Please enter at least ''{0}'' characters

javax.faces.validator.LengthValidator.MAXIMUM={1}: You cannot enter more than ''{0}'' characters

The application message bundle is where we override our validation messages.

Templates:

We have two main templates – one per page. It is instructive to see how tiny they are, given that the elements they contain are generated dynamically.

DynamicPage1.xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"

      xmlns:ui="http://java.sun.com/jsf/facelets"

      xmlns:h="http://java.sun.com/jsf/html"

      xmlns:t="http://myfaces.apache.org/tomahawk">

<head>

    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />

    <title>Dynamic Components - Tutorial - Page1</title>

</head>

<body>

  <h:messages style="color:red;" layout="table" />

  <h:form id="mainForm" prependId="false">

   <t:saveState value="#{dynamicComponentBean.allQuestions}"/>

    <strong>Dynamic Components - Page 1:</strong>

    <h:panelGrid id="mainGrid" binding="#{dynamicComponentBean.mainGrid}" columns="2" />

    <h:commandButton id="submit" type="submit" value="Submit"
              action="#{dynamicComponentBean.processAnswers}"/>

    </h:form>

</body>

</html>

In particular, note the line in blue. This is the only line required in the template to support the entire sample application. The panelGrid’s binding attribute binds the grid itself into its backing bean. This allows the backing bean to conveniently access it using program code (without having to do a findComponent() on the view root, for instance.)

We also saveState the question bank to ensure that we don’t have to go back to the database to retrieve it.

 

DynamicPage2.xhtml:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"

     xmlns:ui="http://java.sun.com/jsf/facelets"

     xmlns:h="http://java.sun.com/jsf/html"

     xmlns:t="http://myfaces.apache.org/tomahawk">

<head>

   <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />

   <title>Dynamic Components - Tutorial - Page1</title>

</head>

<body>

 

<h:form id="mainForm" prependId="false">

   <t:saveState value="#{dynamicComponentBean.answers}"/>

   <t:saveState value="#{dynamicComponentBean.allQuestions}"/>

   <strong>Dynamic Components - Page 2</strong>

   <h:dataTable var="response" value="#{dynamicComponentBean.allResponses}">

       <h:column>

          <h:outputText value="#{response.questionName}"/>:

       </h:column>

       <h:column>

          <h:outputText value="#{response.answer}"/>

       </h:column>

   </h:dataTable>

   <h:commandButton value="Back" action="back"/>

</h:form>

</body>

</html>

This is the second template page, and while it is simpler, it is also slightly beefier than the first. Note that this page is also backed by the same managed bean. The answers are read back in from a read-only property that allows us access to the input provided by the user in the first page.

Java Code:

 

com.swengsol.Question.java

package com.swengsol;

 

import java.io.Serializable;

 

public final class Question implements Serializable {

   private static final long serialVersionUID = -1L;

   private String questionId;

   private String prompt;

   private String questionType;

   private boolean isRequired;

   private String requiredMessage;

   private int minLength;

   private int maxLength;

   public Question(String id, String text, String type) {

       questionId = id; prompt = text; questionType = type;

   }

   public String getQuestionId() { return questionId; }

   public String getPrompt() { return prompt; }

   public String getQuestionType() { return questionType; }

   public boolean isRequired() { return isRequired; }

   public String getRequiredMessage() { return requiredMessage; }

   public int getMinLength() { return minLength; }

   public int getMaxLength() { return maxLength; }

 

   public void setMaxLength(int maxLength) { this.maxLength = maxLength; }

   public void setMinLength(int minLength) { this.minLength = minLength; }

   public void setRequiredMessage(String message) { requiredMessage = message; }

   public void setRequired(boolean required) { isRequired = required; }

}

A Question instance represents a particular element that the user must be prompted to provide. Its main required attributes are a questionId which is used as the component identifier; a prompt; and a questionType that indicates the type of control that should be used to accept user input. For this simple application, we assume that all prompts are based on simple HtmlInputText components.

Additional parameters may be set on a Question that define how this question is validated. We support minimum and maximum lengths; as well as marking a question as being required.

 

com.swengsol.managedbeans.DynamicComponentBean.java

package com.swengsol.managedbeans;

 

import com.swengsol.Question;

 

import javax.faces.component.html.HtmlInputText;

import javax.faces.component.html.HtmlOutputLabel;

import javax.faces.component.html.HtmlPanelGrid;

import javax.faces.context.FacesContext;

import javax.faces.application.Application;

import javax.faces.validator.LengthValidator;

import javax.el.ExpressionFactory;

import javax.el.ELContext;

import javax.el.ValueExpression;

import java.util.Map;

import java.util.LinkedHashMap;

import java.util.ArrayList;

import java.util.List;

 

 

public class DynamicComponentBean {

  private HtmlPanelGrid mainGrid;

  private Map<String, String> answers;

  private List<Question> allQuestions;

 

  public DynamicComponentBean() {

  }

  public void setAllQuestions(List<Question> questions) {

   allQuestions = questions;

  }

 

  public List<Question> getAllQuestions() {

   if (null == allQuestions) {

     System.out.println("Going to the database now ... ");

     allQuestions = new ArrayList<Question>();

     allQuestions.add(
         new Question("firstName", "First Name", HtmlInputText.COMPONENT_TYPE));

     allQuestions.add(
         new Question("lastName", "Last Name", HtmlInputText.COMPONENT_TYPE));

     final Question q =
         new Question("Country", "Country", HtmlInputText.COMPONENT_TYPE);

     q.setRequired(true);

     q.setRequiredMessage("You must enter your country!");

     q.setMinLength(3);

     q.setMaxLength(10);

     allQuestions.add(q);

 

     allQuestions.add(
         new Question("state", "State",   HtmlInputText.COMPONENT_TYPE));

   }

   return allQuestions;

  }

 

  public void addQuestion(HtmlPanelGrid grid, boolean addLabel,
       Question question, String beanName, String propName) {

   if (addLabel) {

     final HtmlOutputLabel label = (HtmlOutputLabel)
         getApplication().createComponent(HtmlOutputLabel.COMPONENT_TYPE);

     label.setId(question.getQuestionId() + "-label");

     label.setFor(question.getQuestionId());

     label.setValue(question.getPrompt());

     grid.getChildren().add(label);

   }

 

   final HtmlInputText htmlInputText = (HtmlInputText)
       getApplication().createComponent(HtmlInputText.COMPONENT_TYPE);

   htmlInputText.setId(question.getQuestionId());

   htmlInputText.setSize(20);

   final String binding =
       "#{" + beanName + "." + propName +"[\"" + question.getQuestionId() + "\"]}";

   final ValueExpression ve2 = getExpressionFactory().
       createValueExpression(getELContext(), binding, String.class);

   htmlInputText.setValueExpression("value",ve2);

 

   if (question.isRequired()) {

     htmlInputText.setRequired(true);

     htmlInputText.setRequiredMessage(question.getRequiredMessage());

   }

   if (question.getMinLength() > 0 || question.getMaxLength() > 0) {

     final LengthValidator validator = (LengthValidator)
         getApplication().createValidator(LengthValidator.VALIDATOR_ID);

     if (question.getMinLength() > 0)

       validator.setMinimum(question.getMinLength());

     if (question.getMaxLength() > 0)

       validator.setMaximum(question.getMaxLength());

     htmlInputText.addValidator(validator);

   }

   grid.getChildren().add(htmlInputText);

  }

 

  public HtmlPanelGrid getMainGrid() {

   if (null == mainGrid) {

     mainGrid = (HtmlPanelGrid)

       getApplication().createComponent(HtmlPanelGrid.COMPONENT_TYPE);

     final List<Question> questions = getAllQuestions();

     for (Question question : questions) {

       addQuestion(mainGrid, true, question, "dynamicComponentBean", "answers");

     }

   }

   return mainGrid;

  }

 

  public void setMainGrid(HtmlPanelGrid mainGrid) {

   this.mainGrid = mainGrid;

  }

 

  public Map<String, String> getAnswers() {

   if (null == answers) {

     answers = new LinkedHashMap<String, String>();

   }

   return answers;

  }

  public void setAnswers(Map<String, String> answers) {

   this.answers = answers;

  }

  public String processAnswers() {

   //do some processing and return the appropriate response

   return "done";

  }

 

  public static ELContext getELContext() {

   return FacesContext.getCurrentInstance().getELContext();

  }

  public static ExpressionFactory getExpressionFactory() {

   return getApplication().getExpressionFactory();

  }

  public static Application getApplication() {

   return FacesContext.getCurrentInstance().getApplication();

  }

 

  public static class QuestionBank {

   private String questionName; private String answer;

   public String getQuestionName() {return questionName;}

   public String getAnswer() {return answer;}

   public QuestionBank(String q, String a) {questionName = q; answer = a; }

  }

 

  //read only property

  public List<QuestionBank> getAllResponses() {

   final List<QuestionBank> responses = new ArrayList<QuestionBank>();

   if (null != answers) {

     for (String key : answers.keySet()) {

       responses.add(new QuestionBank(key, answers.get(key)));

     }

   }

   return responses;

  }

}

This method is where the meat of the action lies, and we’ll take this apart in the following paragraphs.

First up are the data members:

  private List<Question> allQuestions;

  private HtmlPanelGrid mainGrid;

  private Map<String, String> answers;

 

allQuestions is the collection of Question instances that have been retrieved from the database, and which need to be converted into JSF components.

mainGrid is the panel grid referenced by the template which will contain the components associated with the retrieved questions.

answers is a sorted map that contains the answers provided by the user when prompted.

The following line in Page 1 sets up its panel grid as follows:

<h:panelGrid id="mainGrid" binding="#{dynamicComponentBean.mainGrid}" columns="2" />

It is this binding that gives your backing bean direct access to the component’s instance.

The getAllQuestions() method simulates retrieval of the questions from the database … in this case, simply hardcoding them into a List. Four questions are added to this List.

    public List<Question> getAllQuestions() {

        if (null == allQuestions) {

            System.out.println("Going to the database now ... ");

            allQuestions = new ArrayList<Question>();

            allQuestions.add(
              new Question("firstName", "First Name", HtmlInputText.COMPONENT_TYPE));

            allQuestions.add(
              new Question("lastName", "Last Name", HtmlInputText.COMPONENT_TYPE));

            final Question q =
              new Question("Country", "Country", HtmlInputText.COMPONENT_TYPE);

            q.setRequired(true);

            q.setRequiredMessage("You must enter your country!");

            q.setMinLength(3);

            q.setMaxLength(10);

            allQuestions.add(q);

            allQuestions.add(
              new Question("state",  "State",   HtmlInputText.COMPONENT_TYPE));

        }

        return allQuestions;

    }

Three helper methods are provided to help you access functionality specific to the framework. You will need these to create components, bindings, and validators.

  public static Application getApplication() {

   return FacesContext.getCurrentInstance().getApplication();

  }

  public static ELContext getELContext() {
   return FacesContext.getCurrentInstance().getELContext();

  }

  public static ExpressionFactory getExpressionFactory() {

   return getApplication().getExpressionFactory();

  }

 

The getMainGrid() method supports the binding of the h:panelGrid component on the Facelets template to its backing bean. It is concerned with getting the questions and adding each question to the panel grid as a valid component. The panelGrid itself is constructed explicitly – using javax.faces.Application.createComponent(). This factory method takes a logical identifier that indicates the type of component that is being created. Each component defines a static member named COMPONENT_TYPE that holds its logical identifier.

public HtmlPanelGrid getMainGrid() {

  if (null == mainGrid) {

  mainGrid = (HtmlPanelGrid)
       getApplication().createComponent(HtmlPanelGrid.COMPONENT_TYPE);

   final List<Question> questions = getAllQuestions();

   for (Question question : questions) {

     addQuestion(mainGrid, true, question, "dynamicComponentBean", "answers");

   }

  }

  return mainGrid;

}

public void setMainGrid(HtmlPanelGrid mainGrid) {

  this.mainGrid = mainGrid;

}

The heart of this logic lies in addQuestion(), which builds the appropriate components (the label and the field) for each question, and adds it to a panel grid passed in as a parameter.

This method also takes a boolean addLabel field that indicates whether a label should be constructed; the question for which a component is being requested; a beanName and a propName that identifies the bean and the property to which the user-entered value, for this question, should be bound.

Since we don’t know how many questions will be on a given page, I’m using a Map to collect these responses from the user, with the question Id as the key, and the user’s input as the value.

As before, Application.createComponent() is used to create these components. Also, as before, we specify the appropriate COMPONENT_TYPE to indicate the type of field that must be created.

For convenience, the id of the HtmlOutputLabel is set to the question’s id suffixed by “–label”. It is also explicitly linked to its question using its for attribute, and its value is set to the question prompt. Finally, it is added to the panel grid’s list of children.

For the HtmlInputText component there are 2 note worthy things happening: the creation of the value binding expression; and the validation logic.

Next, note the use of the JSF1.2 mechanisms to create a value binding. This has changed substantially since JSF 1.1, and the older mechanisms have been deprecated.

The createValueExpression() call builds a binding expression that points to the answers map – which is a LinkedHashMap<String, String> - using the question’s id as the key into this map.

The setValueExpression() call binds the component’s value attribute to that expression. As a result, a component with the id “question2” will have its value stored in the answers map as the key-value pair <“question2”à“user entered value”>.

Next, we set the mandatory field validator if the question’s isRequired() call returns true.

Finally, we add some additional validators – for minimum and maximum length – just to prove that we can do anything we could do in templating using Java code. For added measure we override the default validation messages using the application’s message bundle – again for the coolness factor.

The question is then added to the grid, and we’re done.

public void addQuestion(HtmlPanelGrid grid, boolean addLabel, Question question,
   String beanName, String propName) {

  if (addLabel) {

  final HtmlOutputLabel label = (HtmlOutputLabel)
       getApplication().createComponent(HtmlOutputLabel.COMPONENT_TYPE);

    label.setId(question.getQuestionId() + "-label");

    label.setFor(question.getQuestionId());

    label.setValue(question.getQuestion());

    grid.getChildren().add(label);

  }

  final HtmlInputText htmlInputText = (HtmlInputText)
   getApplication().createComponent(HtmlInputText.COMPONENT_TYPE);

  htmlInputText.setId(question.getQuestionId());

  final String binding =
   "#{" + beanName + "." + propName +"[\"" + question.getQuestionId() + "\"]}";

  final ValueExpression ve2 =
   getExpressionFactory().createValueExpression(getELContext(), binding, String.class);

  htmlInputText.setValueExpression("value",ve2);

 

  if (question.isRequired()) {

   htmlInputText.setRequired(true);

   htmlInputText.setRequiredMessage(question.getRequiredMessage());

  }

  if (question.getMinLength() > 0 || question.getMaxLength() > 0) {

   final LengthValidator validator =

     (LengthValidator)getApplication().createValidator(LengthValidator.VALIDATOR_ID);

   if (question.getMinLength() > 0)

     validator.setMinimum(question.getMinLength());

   if (question.getMaxLength() > 0)

     validator.setMaximum(question.getMaxLength());

   htmlInputText.addValidator(validator);

  }

  grid.getChildren().add(htmlInputText);

}

 

The last item of note is the QuestionBank and its collected responses. This class serves as a simple way of surfacing the questions to the user on the second page of the application. It is a simple data structure that holds a map of question id to the user’s entry. getAllResponses() is a read only property that allows page 2 to display the map’s entries.

  public static class QuestionBank {

   private String questionName; private String answer;

   public String getQuestionName() {return questionName;}

   public String getAnswer() {return answer;}

   public QuestionBank(String q, String a) {questionName = q; answer = a; }

  }

 

  //read only property

  public List<QuestionBank> getAllResponses() {

   final List<QuestionBank> responses = new ArrayList<QuestionBank>();

   if (null != answers) {

     for (String key : answers.keySet()) {

       responses.add(new QuestionBank(key, answers.get(key)));

     }

   }

   return responses;

  }

Additional Information

The best part of this sample application is that the values entered by the user are remembered even when you navigate back to the previous page. This “magic” of course being engendered by the judicious use of t:saveState on page 2 to ensure that the transient answers Map is still available when you return to the first page.

    <t:saveState value="#{dynamicComponentBean.answers}"/>

 

And you are not limited to adding validators to components. For e.g., you can set a converter, by creating a converter using getApplication().createConverter() and then using the component’s setConverter() method to set the converter on the component; or an action listener on a command component, using addActionListener().

Conclusion

As shown, it is fairly straightforward for developers to directly plug into the component generation mechanism. While this is not the usual mode of operation within JSF, it is nice to know that you can access this level of functionality if your application demands it. This is another example of the many that I’ve found, where JSF really shines.