Problem Statement
Consider the following test code:
<rich:dataTable id="testTable" value="#{dataTableTestBackingBean.rowsModel}" var="rowVar"> <f:facet name="header"> <rich:column> <h:outputText value="Column 1" /> </rich:column> </f:facet> <rich:column> <h:inputText id="valTest" value="#{rowVar.col1}" > <a4j:ajax event="blur" render="testColumn, footerTest" limitRender="true" execute="@this" /> </h:inputText> </rich:column> <rich:column> <h:outputText id="testColumn" value="#{dataTableTestBackingBean.footerValue}" /> </rich:column> <f:facet name="footer"> <rich:column> <h:outputText id="footerTest" value="#{dataTableTestBackingBean.footerValue}" /> </rich:column> </f:facet> </rich:dataTable>
This example will fail with the following Javascript error in the browser:
Error: During update: formId:testTable:0:footerTest not found
This is expected, b/c the correct id for the footer is actually formId:testTable:footerTest
.
The bug is in org.richfaces.context.ComponentIdResolver
. Here is what happens:
- The
RenderComponentCallback
is executed on the input component (‘valTest’ in the example) and it reads the list of short ids to render from the attached Ajax Behavior. Note that we got here by walking the parent data table model, so the current rowKey is 0. RenderComponentCallback
asksComponentIdResolver
to resolve short ids into client IDs- ComponentIdResolver starts ascending up the tree from ‘valTest’ and looking at facets and children until it finds ‘footerTest’.
- At this point it asks for the clientId of ‘footerTest’ w/o regard for the fact that the data model has a rowKey that is set 0
So, we get the wrong id b/c we call UiData.getClientId()
outside of the normal walking order of that model.
I have logged a bug here: https://issues.jboss.org/browse/RF-11134
Workaround
I noticed that if I nest the above data table in another data iterator, then the footer update suddenly starts working. I investigated and it turned out that nested table will have getClientId()
called on all of its components by the parent table, and since clientIds
are cached, by the time ComponentIdResolver
gets to it, it will use the cached id, rather than looking it up at the wrong time. Non-nested tables simply don’t get around to calling getClientId()
on their components before ComponentIdResolver
runs, which is probably the real problem.
Client-side Fix
Until the problem is fixed in RichFaces, the following client-side code fixes the problem. The idea here is to intercept JSF Response handler calls and check if all IDs in the responseXML
are in fact present in the page. If they are not, use the simple ID to find the real clientId
and correct it in the response. After that is done, control is returned to the usual RF Ajax handling code.
YourWebApp/WebContent/resources/javascript/richfaces-ajax-fix.js:
/** * Temporary fix for https://issues.jboss.org/browse/RF-11134 * * Once the ComponentIdResolver or UIDataAdapter.saveChildState is fixed, * this file will no longer be needed. * * @author Val Blant */ if (!window.RichFacesAjaxFix) { window.RichFacesAjaxFix = {}; } if ( typeof jsf != 'undefined' ) { (function($, raf, jsf) { raf.ajaxContainer = raf.ajaxContainer || {}; if (raf.ajaxContainer.jsfResponse) { return; } /** * Save the original JSF 2.0 (or Richfaces) method */ raf.ajaxContainer.jsfResponse = jsf.ajax.response; /** * Check if all IDs in the responseXML are in fact present in the page. * If they are not, use the simple ID to find the real clientId. */ jsf.ajax.response = function(request, context) { var changes = raf.ajaxContainer.extractChanges(request); for (var i = 0; i < changes.length; i++) { var change = changes[i]; var oldClientId = raf.ajaxContainer.getClientId(change); if ( oldClientId == null ) { // This is probably not an "update" change. continue; } var shortId = raf.ajaxContainer.getShortId(change); // Check if this target element actually exists. If not, then we found the bug // var target = document.getElementById(oldClientId); if ( !target ) { // Find the target by shortId targets = jQuery("[id$='" + shortId + "']"); if ( targets.length === 0 ) { // ok.... this target is not on the page at all. Eject. continue; } if ( targets.length === 1 ) { // Only one target found, so we can derive the exact clientId easily var newClientId = targets.first().attr("id"); } else { // This shouldn't happen, b/c nested data tables do not have this bug // // However.... looks like this can happen if we find an element with the same // short id on another tab. // We pick the right one with a VERY BADLY CODED approach - we choose the target with // the id whose length is closest to the original clientId length, b/c they should differ in just // the row index. It's unreliable, but since this is a temporary solution, I'm not spending time to // code this properly. // var smallest_length_delta = 1000; var chosenidx = -1; for (var k = 0; k < targets.length; k++) { var length_delta = oldClientId.length - targets.get(k).id.length ; if ( length_delta < smallest_length_delta ) { chosenidx = k; smallest_length_delta = length_delta; } } var newClientId = targets.get(chosenidx).id; } // Now we have the clientId, so we just need to replace it // in the ID attribute and in the markup itself // if ( newClientId ) { raf.ajaxContainer.fixChange(change, oldClientId, newClientId); } } } // Delegate to the old code // raf.ajaxContainer.jsfResponse(request, context); }; /** * Returns the list of elements under the 'changes' element in the AJAX response */ raf.ajaxContainer.extractChanges = function(request) { var changes = []; var xml = request.responseXML; if (xml !== null) { var pr = xml.getElementsByTagName("partial-response"); if ( pr && pr.length > 0 ) { var responseType = pr[0].firstChild; if (responseType && responseType.nodeName === "changes") { changes = responseType.childNodes; } } } return changes; }; raf.ajaxContainer.getClientId = function(change) { return change.getAttribute('id'); }; /** * Return the id part after the last ':' */ raf.ajaxContainer.getShortId = function(change) { var clientId = raf.ajaxContainer.getClientId(change); var ncs = clientId.split(":"); var shortId = ncs[ncs.length - 1]; return shortId; }; /** * Update the id of the given change element and the content with the newClientId */ raf.ajaxContainer.fixChange = function(change, oldClientId, newClientId) { change.setAttribute("id", newClientId); for (var i = 0; i < change.childNodes.length; i++) { node = change.childNodes[i]; node.nodeValue = node.nodeValue.replace(oldClientId, newClientId); } }; }(jQuery, window.RichFacesAjaxFix, jsf)); }