Programmatically expanding and collapsing an af:panelSplitter

Introduction

The ADF Faces af:panelSplitter is a component with client-side behavior, when you expand or collapse the splitter in the browser, no request is sent to the server. This imposes some challenges when you want to change the splitter state in Java code at the server-side as explained in this article.

Main Article

Last week a customer asked me for help to implement a scenario where the search area part, displayed in the top part of an af:panelSplitter should be collapsed when the user presses the search button, in order for the results part, shown in the bottom part of the af:panelSplitter to take as much screen real estate as possible. Quite easy at first sight, just bind the collapsed property of the splitter to a boolean managed bean property and change the value of this property when clicking the search button. Got it to work in 5 minutes, but…. it only worked the first time. After we manually expanded the top part of the splitter again and clicked the search button, the top part no longer auto-collapsed. We figured this was because the value of our collapsed managed bean property was no longer in synch with the actual splitter collapsed state. I first looked for a disclosureListener property on the splitter to keep it in synch but to my (initial) surprise there is no such property. On second thought this makes perfect sense though: expanding or collapsing a splitter in the browser is a client-side only action, nothing happens on the server. So, we needed to trigger a server action when collapsing the splitter to synchronize the server-side state of the splitter. I will explain the steps using this little sample:

Splitter

Here is the page source of this sample:

<af:document id="d1">
  <f:facet name="metaContainer">
    <af:resource source="/js/lib.js" type="javascript"></af:resource>
  </f:facet>
  <af:form id="f1">
    <af:panelSplitter id="ps1" orientation="vertical"
                      collapsed="#{viewScope.SplitterBean.collapsed}">
      <f:facet name="first">
        <af:panelHeader text="Splitter Collapse Test" id="ph1">
          <af:panelGroupLayout id="pgl1" layout="scroll">
            <af:commandButton text="Collapse this part" id="cb1">
              <af:setActionListener from="#{true}"
                                    to="#{viewScope.SplitterBean.collapsed}"/>
            </af:commandButton>
          </af:panelGroupLayout>
        </af:panelHeader>
      </f:facet>
      <f:facet name="second">
        <af:panelHeader text="Second part of splitter" id="ph2">
          <af:panelGroupLayout id="pgl2" layout="scroll">
            <af:commandButton text="Expand top part" id="cb2">
              <af:setActionListener from="#{false}"
                                    to="#{viewScope.SplitterBean.collapsed}"/>
            </af:commandButton>
            <af:commandButton text="Collapse top part" id="cb3">
              <af:setActionListener from="#{true}"
                                    to="#{viewScope.SplitterBean.collapsed}"/>
            </af:commandButton>
          </af:panelGroupLayout>
        </af:panelHeader>
      </f:facet>
      <af:clientListener type="click" method="splitterClick"/>
      <af:serverListener type="handleSplitterClick"
                         method="#{viewScope.SplitterBean.handleSplitterEvent}"/>
    </af:panelSplitter>
  </af:form>
</af:document>

And here is the code of the splitterBean:

public class SplitterBean
{
  boolean collapsed = false;
  public void setCollapsed(boolean collapsed)
  {
    this.collapsed = collapsed;
  }
  public boolean isCollapsed()
  {
    return collapsed;
  }
  public void handleSplitterEvent(ClientEvent clientEvent)
  {
    RichPanelSplitter splitter = (RichPanelSplitter) clientEvent.getComponent();
    boolean serverSideCollapsedState = splitter.isCollapsed();
    // server side state is not yet in synch with client state
    // so we set the reverse value for collapsed.
    splitter.setCollapsed(!serverSideCollapsedState);
    setCollapsed(!serverSideCollapsedState);
  }
}

And the javascript library function:

function splitterClick(evt) 
{ 
  AdfCustomEvent.queue(evt.getSource(), "handleSplitterClick",{}, true);
  evt.cancel();
}

This is how this works:

  • When manually expanding the splitter in the browser, the clientListener fires.
  • The clientListener calls the splitterClick Javascript method which queues an event picked up by the serverListener.
  • The serverListener calls the server-side handleSplitterEvent method in the splitter managed bean
  • The handleSplitter method synchronizes the client-side splitter state with the server-side state

You can download the sample application here (JDeveloper 11.1.1.6).

Comments

  1. Gabriel Copley says:

    Jdeveloper 11.1.1.7
    Weblogic 10.3.6

    I have the same change I would like in my application, and like Navaneet, the above code did not work in my jdeveloper. My solution was as follows:
    Page Source:
    Line 7 change to binding=”#{viewScope.SplitterBean.splitter}”>

    SplitterBean
    Line 3 change to private RichPanelSplitter splitter; add getters/setters.
    Line 6 of change to: splitter.setCollapsed(collapsed);
    Line 10 changes to: return splitter.isCollapsed();
    Delete lines 18 & 19 of handleSplitterEvent. Or replace with “setCollapsed(isCollapsed());”

    In my case, by writing “setCollapsed(isCollapsed());” I can put my handler code in the set function and change other parts of the page with PPR.

  2. Navaneet Agarwal says:

    Hi

    I tried using the above approach but there is an issue with this. Even if you click on an empty area on the panelSplitter, the server side value changes! After clicking on an empty area, if you refresh the page (if you are not partial rendering the complete splitter that is), you’ll see that the collapsed state of the splitter has now inverted!

Add Your Comment