2009/5/25 Andy Schwartz <andy.schwartz@oracle.com>
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. 

I believe so.
 
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?

Okay, sure.
 
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.

I agree that it's the preferred approach, however, interestingly enough, I cannot get it to work. I think I'm doing  essentially the same thing that you are doing above. Hmmmm.

I already sent Ryan a war, but I've added explicit ids to all my containers, so I will send  him an updated war and cc you.

Thanks for all your help, I really appreciate it.


david


Andy


David Geary wrote On 5/25/2009 1:36 PM ET:
2009/5/24 Jim Driscoll <Jim.Driscoll@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