Wednesday, 27 May 2009

JSF: using component IDs in a data table (h:dataTable vs clientId)

Updated 2009/11/24

This post is obsolete; go read this one instead: JSF: working with component identifiers The approach described in this post may fail if the component identifiers are not unique within the view.


I've written about how to use a custom function to get the clientId before. You can download the code from here.

Below is an example that uses the id:cachedClientId function in a data table. I'm using Facelets, so you can't just cut and paste the code into a JSP. However, the principle is the same.

screenshot

The page lists a number of products and lets you set the fields to default values using JavaScript. Not a brilliant example, but it will suffice.

The Facelets page:

<?xml version="1.0" encoding="UTF-8" ?>
<!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:id="http://illegalargumentexception.googlecode.com/clientId">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>clientIdTest</title>
</head>
<body>
<h:form>
  <h:dataTable value="#{dataBean.rows}" var="row">
    <h:column>
      <h:outputText value="#{row.product}" />
    </h:column>
    <h:column>
      <h:inputText id="qtty" value="#{row.quantity}" />
    </h:column>
    <h:column>
      <button
        onclick="setQuantity('#{id:cachedClientId('qtty')}', #{row.defaultQuantity}); return false">
      Set as #{row.defaultQuantity}</button>
    </h:column>
  </h:dataTable>
  <h:commandButton value="order" action="#{dataBean.order}" />
</h:form>

<script type="text/javascript">
function setQuantity(elementId, n) {
   var textField = document.getElementById(elementId);
   textField.value = n;
   return false;
} 
</script>

</body>
</html>

The demo table is backed by by this session bean:

/*
 <managed-bean>
 <managed-bean-name>dataBean</managed-bean-name>
 <managed-bean-class>beans.DataBean</managed-bean-class>
 <managed-bean-scope>session</managed-bean-scope>
 </managed-bean>
 */
public class DataBean implements Serializable {

  private static final long serialVersionUID = 1L;

  private final List<RowBean> rows = new ArrayList<RowBean>();

  public DataBean() {
    for (int i = 0; i < 5; i++) {
      RowBean row = new RowBean();
      row.setProduct("Product " + i);
      row.setDefaultQuantity(i);
      rows.add(row);
    }
  }

  public List<RowBean> getRows() {
    return rows;
  }

  public String order() {
    // TODO: order logic
    System.out.println("ordering....");
    return null;
  }

}

Here is the row definition:

public class RowBean implements Serializable {

  private static final long serialVersionUID = 1L;

  private String product;
  private int defaultQuantity;
  private int quantity;

  public String getProduct() {
    return product;
  }

  public void setProduct(String product) {
    this.product = product;
  }

  public int getDefaultQuantity() {
    return defaultQuantity;
  }

  public void setDefaultQuantity(int defaultQuantity) {
    this.defaultQuantity = defaultQuantity;
  }

  public int getQuantity() {
    return quantity;
  }

  public void setQuantity(int quantity) {
    this.quantity = quantity;
  }
}

How does that work?

Here is how the HTML table rows are rendered:

<tr>
    <td>
        Product 4
    </td>
    <td>
        <input id="j_id2:j_id3:4:qtty" name="j_id2:j_id3:4:qtty" type="text" value="4" />
    </td>
    <td>
        <button onclick="setQuantity('j_id2:j_id3:4:qtty', 4); return false">
        Set as 4</button>
    </td>
</tr>

The clientId of the text field (id=qtty) is managed by its parent naming containers (e.g. UIData). The data table will change the clientId of its children before each row is rendered. Because the caching mechanism in the expression #{id:cachedClientId('qtty')} caches the path to the component and not the clientId value, it returns the correct value each time.

How to screw things up

Here is an example of how not to refer to the clientId. Here, we try to rewrite the Facelet to insert the ID directly into the JavaScript:

<script type="text/javascript">
function setQuantity(n) {
   var textField = document.getElementById('#{id:cachedClientId('qtty')}');
   textField.value = n;
   return false;
} 
</script>

However, this does not work! This is what is rendered to the page:

<script type="text/javascript"><!--
function setQuantity(n) {
   var textField = document.getElementById('j_id2:j_id3:qtty');
   textField.value = n;
   return false;
} 
//--></script>

The HTML element ID j_id2:j_id3:qtty does not match any of the text fields (which look have clientIds of the form j_id2:j_id3:N:qtty). The expression is not being evaluated in the context of any of the rows. It is an error to use the expression #{id:cachedClientId('qtty')} outside the table that controls that component.

Notes

I used Apache MyFaces 1.2.3 and Facelets 1.1.14 running on Tomcat 6 under Java 6.

2 comments:

  1. Hi, I'm using a table that has a column of textfields. When the user clicks a button, I want to retrieve the new values from each field. I know the client ID of the components, however I can't find them in my bean. Do you know how to do this?
    Thanks

    ReplyDelete
  2. That's kind of vague - I suggest you post a question on stackoverflow.com (or some similar site) with more details.

    ReplyDelete

All comments are moderated