Wednesday, October 3, 2012

tr:forEach to soon provide support for changes to the collection

In order to address the issues that I discussed in my blog regarding the c:forEach tag, I implemented enhancements to the Trinidad forEach tag since I am a developer on that team.

The changes are part of TRINIDAD-1940 which should be part of the future Trinidad 2.1.0 release. Until then, you can try out the changes by using the snapshot build.

The forEach tag documentation does not have the new documentation as the site is currently based on the 2.0.2-SNAPSHOT documentation.

Until then, here is a preview of the new forEach documentation (Content owned by the ASF is covered by the Apache version 2.0 license):

Summary

Tag name: <tr:forEach>

The forEach tag provides additional functionality from the JSTL <c:forEach> tag. The tag is only supported in JSP.

Facelets

The <tr:forEach> is not supported in facelets. As a convenience, the tag handler for the <c:forEach> will be used in facelets when a <tr:forEach> is used in a facelets page, but no Trinidad functionality will be supported.

Supported collections

The Trinidad forEach tag supports the following collections for the items attribute:
  • Java array
  • java.util.List
  • org.apache.myfaces.trinidad.model.CollectionModel
  • java.util.Map

When using a map, the map should be one that has a consistent order. Either the java.util.LinkedHashMap or java.util.TreeMap are examples of maps that provide this functionality. The keys for the maps must implement java.io.Serializable.

Usage

Valid Use Cases

The for each tag is not as efficient as component stamping. When possible, the <tr:iterator> should always be used instead of the for each tag. There are times this is not possible and usually involve the inclusion of content from other pages. For example, if different <jsp:include> tags need to be generated, pointing to different files based on items in a list, a for each tag must be used and not a stamping component like the Trinidad iterator.

ID and Component State

Due to the way JSF and JSP create ValueExpression objects, the for each tag cannot safely map the expressions created and stored on JSF components to the iteration of the for each loop. As a result, it is possible to cause a discrepency between the component's location in the for each tag and the value expressions that are stored in the tag.

Due to this limitation, the forEach tag must place usage requirements on the pages they are placed on. For index based collections (arrays and lists), the components must be mapped to the index of the for each loop and not the item in the collection. In these cases, the index of the for each loop is stored into the value expressions created and stored into the components. This results in the component state (like expanded state of disclosure components) being tied to the index of the for each loop. As a result, a key based collection (map or collection model) is always recommended when the collection may change between requests.

For key based collections (map and collection model), the component IDs must be associated with the key of the collection. This is because the key of the collection is used in the value expressions and if the ID were not bound to the key, then the component state would not match the EL values. You should associate the component to the key using immediate EL in the ID attribute. See the examples below.

Generated IDs

When the collection may change between requests, a key based collection should be used. When this is done, it is not recommended that JSF be allowed to generate the IDs of any components inside the for each tag. The reason is that JSF auto-generated IDs are based on the sequence that they are created in the current page. If items are re-arranged in the for each loop, the IDs will have a different generation order. As a result, JSF will not find the components with auto-generated IDs when it tries to map the JSP tag to the component ID. Existing components will be thrown out and new ones will be created and component state will be lost if this occurs.

Reordering and Collection Modifications

Modifications to the collection in the same JSF view (between requests) are only supported with key based collections. Even with this being supported, the for each tag will not auto-rearrange the children.

When JSP seeks to locate a component that was created in a previous request, it creates the ID that it expects the component to have and then searches the children of the parent component of the tag for the child component with that ID. If the component is not found, a new one will be created. If it is found, the framework does not do anything to the component, it is left to exist where it is found. This means that if you reorder items in your collection between requests, the component order will no longer match the order of the items in the collection. Page authors must reorder the components when changes to the collection are made.

Trinidad supports re-ordering of components using the change manager (org.apache.myfaces.trinidad.change.ChangeManager) and the org.apache.myfaces.trinidad.change.ReorderChildrenComponentChange class. The change manager may be retrieved from the org.apache.myfaces.trinidad.context.RequestContext instance. See the Trinidad demo application for examples on how to reorder the components when the collections changes.

When using a key based collection, it is recommended to use a naming container to simplify tying the component ID to the key. See the examples below.

Examples

Please see the Trinidad demo application for complete examples.

Index Based Collection

<tr:forEach var="person" items="#{forEachBean.simpleList}" varStatus="vs">
  <f:subview id="sv${vs.key}">
    <tr:panelGroupLayout id="personPgl" layout="horizontal">
      <tr:outputText id="personName" value="#{person.firstName} #{person.firstName}"
                     inlineStyle="padding-right: 1em;"/>
      <tr:outputText id="clientId" value="(Client ID: #{component.clientId})"/>
    </tr:panelGroupLayout>
    <tr:spacer id="s1" height="1em" />
  </f:subview>
</tr:forEach>

Key Based Collection

<tr:forEach var="person" items="#{forEachBean.collectionModel}" varStatus="vs">
  <f:subview id="sv${vs.key}">
    <tr:panelGroupLayout id="personPgl" layout="horizontal">
      <tr:outputText id="personName" value="#{person.firstName} #{person.firstName}"
                     inlineStyle="padding-right: 1em;"/>
      <tr:outputText id="clientId" value="(Client ID: #{component.clientId})"/>
    </tr:panelGroupLayout>
    <tr:spacer id="s1" height="1em" />
  </f:subview>
</tr:forEach>

Example Re-Order Code

// Create a list of all the children IDs of the parent component of the for each loop. This must
// contain all the children, not just those created by the for each tag.
List<String> orderedIds = ...;

// Get the reference to the component that is the parent of the for each tag. For finding relative
// components, you may try using
// org.apache.myfaces.trinidad.util.ComponentUtils.findRelativeComponent(UIComponent, String)
UIComponent forEachParent = ...;

ReorderChildrenComponentChange componentChange = new ReorderChildrenComponentChange(orderedIds);
RequestContext requestContext = RequestContext.getCurrentInstance();
ChangeManager cm = requestContext.getChangeManager();
// The component change must be added before the tag execution (before the render response JSF
// phase)
cm.addComponentChange(FacesContext.getCurrentInstance(), forEachParentComponent,
  componentChange);

// Ensure that the view is updated during PPR requests
requestContext.addPartialTarget(forEachParentComponent);
        

Attributes

Name Type Supports EL? Description
begin primitive int or java.lang.Number subclass No index at which iteration begins
end primitive int or java.lang.Number subclass No index at which iteration ends
items Object Only EL the collection to iterate over. Supported classes:
  • Java array
  • java.util.List
  • org.apache.myfaces.trinidad.model.CollectionModel
  • java.util.Map
step primitive int or java.lang.Number subclass No number to increment the index by on each iteration
var String No name of the variable exposed when iterating that references the item in the collection
varStatus String No name of the loop status exposed when iterating.

Properties:

Name Description
index the current index (int)
count the total number of times the for each tag will iterate (int)
begin the index to start at when collections are not used (int)
end the last index when collections are not used (int)
first true if this is the first iteration (boolean)
last true if this is the last iteration (boolean)
key the index (int) for index based collections and when the items has not been specified. The key (java.io.Serializable) for key based collections

No comments: