[jsr-314-open] Ajax rendering of components among compositions?

Andy Schwartz andy.schwartz at ORACLE.COM
Mon May 25 18:43:16 EDT 2009


Gang -

I spent the last couple of hours testing out various composite 
component/f:ajax/id-related use cases, using the ajax-switchlist demo 
(switchlistAjaxComponent.xhtml) as my base test case.  The 
ajax-switchlist component hierarchy looks like this (simplified down to 
significant parts):

> <h:body>
>   <h:form id="form1">
>     <ez:switchlist id="switchlist"/>
>     <h:commandButton value="reload" type="submit"/>
>   </h:form>
> </h:body>


And <ez:switchlist> composite component contains the following (again, 
stripped down to interesting parts):

>   <cc:implementation>
>
>     <h:selectManyListbox id="list1"/>
>
>     <h:commandButton id="move1to2">
>       <f:ajax execute="@this #{cc.clientId}:list1" 
> render="#{cc.clientId}:list1 #{cc.clientId}:list2"/>
>      </h:commandButton>
>
>     <h:selectManyListbox id="list2"/>
>
>   </cc:implementation>


First thing to note...  The execute/render ids are specified in an 
overcomplicated manner.  As I mentioned in email earlier today, the 
f:ajax execute/render ids are spec'ed to adhere to findComponent() id 
resolution semantics.  Since the <f:ajax> behavior is attached to a 
component that is in the same naming container as selectManyListbox 
components that we are targeting, we can simplify from this:

>       <f:ajax execute="@this #{cc.clientId}:list1" 
> render="#{cc.clientId}:list1 #{cc.clientId}:list2"/>


To:

>       <f:ajax execute="@this list1" render="list1 list2"/>


Ah, that feels much better.  Actually, this isn't just a 
simplification... this is correct solution.  Unfortunately the original 
code only happens to work due to non-spec compliant implementation 
behavior.  That is, when we specify:

>   render="#{cc.clientId}:list1"


This is the same as specifying:

>   render="form1:switchlist:list1"


Which should have the same behavior as calling:

>   move1to2ButtonComponent.findComponent("form1:switchlist:list1");


This will result in the search expression "form1:switchlist:list1" being 
resolved relative to the nearest naming container, which in this case is 
the switchlist composite component itself.  Since there is no 
"form1:switchlist:list1" component within the composite component naming 
container, findComponent() would return null, and the <f:ajax> tag 
should log a warning.

However, instead of failing/logging a warning, we've got some code in 
AjaxBehaviorRenderer.findComponent() that does the following:

>         UIComponent resolvedComponent = component.findComponent(expr);
>         if (resolvedComponent == null) {
>             // not found using a relative search, try an absolute search
>             resolvedComponent = component.findComponent(':' + expr);
>         }

The bonus absolute search is not part of the spec - ie. is not typical 
findComponent() id resolution behavior.  This code should be removed and 
replaced by a warning.  (We should be able to make the warning clearer 
by identifying the naming container that provided the scope of the search.

In any case, when switching over to:

>       <f:ajax execute="@this list1" render="list1 list2"/>


Everything works as expected.  We definitely should update this demo to 
use this simpler, more spec-compliant solution.

In order to try to reproduce the problem that David was seeing, I 
decided to see whether I could update the "reload" button on the main 
page from within the composite component.  So, first I gave the reload 
button an id:

>     <h:commandButton value="reload" type="submit" id="reload"/>


And then I added a new render target inside of our <f:ajax> tag (inside 
of the composite component):

>       <f:ajax execute="@this list1" render="list1 list2 
> :#{cc.parent.clientId}:reload"/>


Note that I used the leading ":" character to indicate that this is an 
absolute id.

I was thinking that "#{cc.parent}" would resolve to the composite 
component's immediate parent, which in this case happens to be the 
"form1" component.  As such, I figured that "#{cc.parent.clientId}" 
would give me "form1", and ":#{cc.parent.clientId}:reload" would produce 
the absolute id ":form1:reload", which should target the outer reload 
button (leaving aside the question of whether this is a good thing to do 
or not).

However, when I ran the test case, I hit the following error:

> <f:ajax> contains an unknown id '::reload'


Okay, that's weird.  So this means that "#{cc.parent.clientId}" is not 
resolving to the parent component's client id, but instead to null/empty 
string.  Huh.

After poking around some more, I found my way to the 
com.sun.faces.el.CompositeComponentAttributesELResolver, which provides 
some special handling for certain properties on the "cc" object, 
including the "parent" property:

>             if (COMPOSITE_COMPONENT_PARENT_NAME.equals(propertyName)) {
>                 UIComponent c = (UIComponent) base;
>                 context.setPropertyResolved(true);
>                 UIComponent ccParent = 
> UIComponent.getCompositeComponentParent(c);
>                 return ccParent;
>             }


And, of course, UIComponent.getCompositeComponentParent() does not 
return the composite component's parent, but instead returns the nearest 
ancestor composite component.  Which, in this particular demo, is... 
null.  Yuck.  Okay, in retrospect, rather than re-defining the meaning 
of "parent" here, we should have introduced a new derived property, eg. 
"compositeParent", or something along those lines.  (Unfortunately the 
current behavior is defined by the spec - section 5.6.2.2, so too late 
to do anything about this for 2.0.)

I thought I might be able to work around this by avoiding the reference 
to the "cc" implicit object - ie. thought I might be able to get this to 
work by using the "component" implicit object instead of the "cc" 
object, eg: ":#{component.parent.parent.clientId}:reload".  But I ended 
up running into the same problem.

So, David - it is possible that you are running into this as well.  It's 
kind of tough to tell with the auto-generated ids in your content.  Any 
chance you could add explicit ids to the naming containers in your 
sample just to make it easier to understand the client ids that are 
being rendered?

On a positive note, I was able to manually pass in the reload button id 
to the composite component without any problems.  To do this, I added 
the following to the composite:interface:

>     <cc:attribute name="reloadId"/>


And the following to the composite:implementation:

>         <f:ajax execute="@this list1" render="list1 list2 
> #{cc.attrs.reloadId}"/>


And then updated the main page to specify the new attribute:

>             <ez:switchlist id="switchlist" reloadId=":form1:reload"/>


This worked without any problems.

David - since I think we all agree that this is the preferred approach, 
and since there doesn't seem to be any way to get at a composite 
component's actual parent (unless I am missing something), this seems 
like the right way to go for your J1 demo.  If you continue to have 
problems with this approach, maybe cc me when you send Ryan your war.  
Don't know how much time I'll have to take a look, but I am definitely 
curious.

Andy


David Geary wrote On 5/25/2009 1:36 PM ET:
> 2009/5/24 Jim Driscoll <Jim.Driscoll at sun.com 
> <mailto:Jim.Driscoll at sun.com>>
>
>     On 5/24/09 8:39 PM, David Geary wrote:
>
>         And that id is *exactly* what <f:ajax> says it cannot find.
>         It's in the
>         page, but <f:ajax> cannot find it?!?
>
>         My guess is that <f:ajax> is evaluating the id before the page is
>         completely constructed, and therefore, it doesn't find it, but
>         that's
>         just a WAG on my part.
>
>         So, if this is not correct:
>
>         <f:ajax render="#{cc.parent.clientId}:image"/>
>
>         Then how do I access the image in the parent (map) component?
>
>         It seems to me that I'm using <f:ajax> correctly, but I'd be
>         happy to be
>         told otherwise.
>
>
>
>     It's late, but yes, that looks correct, and yes, your guess sounds
>     correct.  Could you file this as an impl bug?
>
>
> Okay. I'll send a WAR to Ryan, and see if we can get this fixed asap. 
> I'd like to get this working for my J1 demo.
>
>
>
>     Now, we do need to fix that as a bug, but I must argue that your
>     use case represents bad practice.  Your component is using ajax
>     inside a component to modify the using page.  That's difficult to
>     maintain, and I suspect it will lead to obfuscated code quite
>     easily.  I'd instead argue that if you are going to have one
>     component modify another, you should either group them into one
>     composite, or, have the ID of the component you act upon passed in
>     as an attribute (say "for="?)
>
>
> Yup, I concur--too much coupling between parent and child components. 
> Unfortunately, I can't seem to pass the component ID to the child 
> component either. IOW, this doesn't work:
>
> In parent component:
>
>   <places:mapZoomControl componentToRender="#{cc.clientId}:image"/>
>
> And in mapZoomControl.xhtml:
>
>   <composite:attribute name="componentToRender"/>
>   ...
>   <f:ajax render="#{cc.attrs.componentToRender}"/>
>
> I suppose it's not surprising that the preceeding doesn't work--it 
> looks like a manifestation of the same bug. I'll give Ryan a buzz.
>
> Thanks,
>
>
> david
>
>
>
>     Of course, that second usage suggestion opens up another hole in
>     the cc api - the lack of ability to do name resolution.  Though
>     if/when we get method parameters, then cc.findComponent will work,
>     I guess.    Though I've been saying that for a pretty long time
>     now, I should check on when we're getting those method params...
>
>     Jim
>
>

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.jboss.org/pipermail/jsr-314-open-mirror/attachments/20090525/ca75ea59/attachment.html 


More information about the jsr-314-open-mirror mailing list