Home > Web development > JSF2: Finding and re-rendering deeply nested components in different NamingContainers

JSF2: Finding and re-rendering deeply nested components in different NamingContainers

In complex JSF web applications, you sometimes need to re-render (i.e. update via AJAX) a component located outside of the current form (note that forms should not be nested). While you can access a composite’s parent in EL using the #{cc.parent} attribute, this is not of much help if your component is deeply nested in a different sub-tree of the component hierarchy. The apparent alternative here is to reference the component by its absolute path, like this (note the leading colon):

:j_id1:j_id3464989:j_id9021841:j_id0491839:basketArea)

However, this approach has got two downsides:

  1. For each composite component, a new UINamingContainer is generated. This means that the more composites you nest into each other, the deeper your hierarchy (and the longer the component path) becomes.
  2. This leads us to the second issue: As soon as you change the structure of your component hierarchy only slightly — i.e. you decide to remove a nested PrimeFaces panel and put the button directly into its parent container — the component path changes. Which means that you have to adjust the path at every place where this button is referenced. This is especially a disadvantage in very agile software projects, where the page layout is not stable (e.g. research projects).

After a helpful discussion on Stack Overflow, I ended up with the following two approaches to this problem:

Resolving components by their ID

It would be much more convenient if you could just reference a component by its ID, instead of specifying the entire path. This is, of course, only possible if we can guarantee that the ID is unique for the entire component tree. Normally, a component ID only has to be unique within its naming container.

If we can assume the uniqueness of these IDs, a EL expression containing a call to a bean method should do the trick:

#{componentResolver.resolve('sidebarBasketArea')}

Within this method, locating the corresponding component is only a matter of traversing the component tree. If a component ID matches the one that we are looking for, we determine the component’s absolute path by concatenating its parent IDs from the root of the component tree down to the component we found, using “:” as the separator character.

The following listing shows how such a Component Resolver class could be implemented (double-click for copy & paste):

import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;
import javax.inject.Named;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ComponentResolver {
	/**
	 * Returns the absolute path of a given {@link UIComponent}.
	 * E.g. :namingContainer1:namingContainer2:componentId.
	 *
	 * @param currentComponent
	 * @return The absolute path of the given component
	 */
	public static String getAbsoluteComponentPath(UIComponent currentComponent) {
		final char separatorChar = UINamingContainer.getSeparatorChar(FacesContext.getCurrentInstance());

		String path = "";
		if(!(currentComponent instanceof NamingContainer)) path = currentComponent.getId();

		do {
			if(currentComponent instanceof NamingContainer) {
				path = currentComponent.getId() + (!path.isEmpty() ? separatorChar : "") + path;
			}
			currentComponent = currentComponent.getParent();
		}
		while(currentComponent != null);
		path = separatorChar + path;

		return path;
	}

	/**
	 * Returns a whitespace-separated list of the absolute paths of all given components.
	 *
	 * @param components The list of components
	 * @return A whitespace-separated list of all absolute paths.
	 * @see ComponentResolver#getAbsoluteComponentPath(UIComponent)
	 */
	public static String getAbsoluteComponentPaths(Collection<UIComponent> components) {
		String paths = "";
		for(UIComponent c : components) {
			if(!paths.isEmpty()) paths += " ";
			paths += ComponentResolver.getAbsoluteComponentPath(c);
		}
		return paths;
	}

	/**
	 * Finds all components with the given id, and returns a whitespace-separated list
	 * of their absolute paths (e.g. <code>:j_id1:j_id2:myId :j_id1:j_id3:myId</code>).
	 *
	 * @param id The id to search the component tree for
	 * @return Whitespace-separated list of absolute component paths
	 * @see #resolveList(String)
	 */
	public static String resolve(String id) {
		List<UIComponent> components = resolveList(id);
		return getAbsoluteComponentPaths(components);
	}

	/**
	 * Finds all components with the given id by traversing the component tree.
	 * @param id Id to search for.
	 * @return List of components that have got this id.
	 */
	public static List<UIComponent> resolveList(String id)
	{
		return resolveList(id, FacesContext.getCurrentInstance().getViewRoot());
	}

	/**
	 * Recursive function that traverses the component tree and accumulates all components
	 * that match the given id.
	 * @param id The id to look for
	 * @param currentComponent Component of the tree to start traversal at
	 * @return Accumulation of all components that match the given id
	 */
	private static List<UIComponent> resolveList(String id, UIComponent currentComponent) {
		List<UIComponent> accumulator = new LinkedList<UIComponent>();
		
		if(null != currentComponent.getId() && currentComponent.getId().equals(id)) {
			accumulator.add(currentComponent);
		}

		Iterator<UIComponent> childIt = currentComponent.getFacetsAndChildren();
		while(childIt.hasNext()) {
			UIComponent child = childIt.next();
			accumulator.addAll(resolveList(id, child));
		}

		return accumulator;
	}
}

Note that when traversing the component tree, it is necessary to call UIComponent#getFacetsAndChildren(), and not just UIComponent#getChildren().

Please note that this approach fails if you want to uniquely identify an element embedded into a ui:repeat tag (e.g. the 4th element generated by that loop). This is because ui:repeat does not create a separate UINamingContainer for each iteration, and thus, the repeated content has no unique ID.

Component map in a backing bean

A completely different approach employs JSF’s binding feature, which allows us to bind a UI component to a field of a backing bean. Imagine a request-scoped bean with the following field and the corresponding getter:

private Map<String, UIComponent> componentMap;
public Map<String, UIComponent> getComponentMap() {
  return componentMap;
}

Now, we can bind an arbitrary component in our Facelets XHTML page to an entry in the component map:

<h:inputTextarea binding="#{backingBean.componentMap['myTextArea']}" />

Note, however, that the backing bean must be request-scoped in this case, because the component tree usually gets re-built by JSF after each request, making references to components from the previous requests invalid.

Categories: Web development Tags:
  1. No comments yet.
  1. No trackbacks yet.
You must be logged in to post a comment.