Matyas
Matyas

Reputation: 13702

Why do nested dynamic accordion panels lose their activeIndex information on @form rerender?

I have the following structure

<h:form>
<!-- some elements -->
<p:accordionPanel id="outer" multiple="true" var="node" dynamic="false" value="#{model.nodes}">
    <p:tab id="outerId">
        <p:accordionPanel id="inner" multiple="true" dynamic="false" var="child" value="#{node.children}">
            <p:tab id="innerId">
              <!-- iterated components -->
            </p:tab>
        </p:accordionPanel>
    </p:tab>
</p:accordionPanel>
</h:form>

Then I tried to use in ajax postback & rerender from within the inner tabs & from the outside of the whole hierarchy:

<f:ajax event="click" render="@form" execute="@form" />

or

<p:ajax process="@form" partialSubmit="false" update="@form" />

The problem was that the active indexes got persisted only for the outer accordion, but not the child accordions (though looking at the POST data, child accordion active indexes are sent as well). Am I doing something wrong or I shouldn't expect this to work out of the box?

What would work

On other hand I know that I could manage manually the active indexes by providing for example a field on model and every node, that would persist this data. (Didn't test but after digging through a lots of pf posts/so pages that's what I'd expect)

<p:accordionPanel activeIndex="#{model.activeIndex}"...
       <p:accordionPanel activeIndex="#{node.activeIndex}"...

Can anyone confirm that this second approach is the only way around? Or I'm doing something wrong in the first case?

Primefaces 3.4.2

Glassfish stack 3.1.2.2

UPDATE 05.04.2013

The latter approach doesn't work either. Because on ajax POST node.activeIndex receives the value "" (and only the root active index is set correctly)

Data scenario (POST data details)

  1. Load the page.
  2. Open the first two outer tabs.
  3. Open the first two inner tabs from the 2nd outer tab.
  4. Click on an element within the page
  5. POST data
    javax.faces.partial.ajax=true
    javax.faces.source=j_idt106:j_idt271:1:j_idt121:j_idt110:0:j_idt113:featureRepeater:11:featureCheckboxP
    javax.faces.partial.execute=gridDetailPage
    javax.faces.partial.render=gridDetailPage
    javax.faces.behavior.event=valueChange
    javax.faces.partial.event=change
    gridDetailPage=gridDetailPage
    j_idt106:j_idt271:1:j_idt121:j_idt110_active=0,1   // INNER OPEN TABS
    j_idt106:j_idt271_active=0,1                       // OUTER OPEN TABS
    javax.faces.ViewState=4232962649695633063:-8633977119414123467
  1. The page that is rendered has the 1st two outer tabs, and only the 1st inner tab from the second outer tab open (WRONG)
  2. A following POST is only POSTing the current (WRONG) configuration
javax.faces.partial.ajax=true
javax.faces.source=j_idt106:j_idt271:1:j_idt121:j_idt110:0:j_idt113:featureRepeater:0:featureCheckboxP
javax.faces.partial.execute=gridDetailPage
javax.faces.partial.render=gridDetailPage
javax.faces.behavior.event=valueChange
javax.faces.partial.event=change
gridDetailPage=gridDetailPage
j_idt106:j_idt271:1:j_idt121:j_idt110_active=0   // INNER OPEN TABS
j_idt106:j_idt271_active=0,1                     // OUTER OPEN TABS
javax.faces.ViewState=4232962649695633063:-8633977119414123467

Upvotes: 1

Views: 3636

Answers (1)

Matyas
Matyas

Reputation: 13702

Solution proposed by andyba:

"In order to ensure that the activeIndex values are being propagated you need to tickle the accordionPanel, you do this by simply adding an empty p:ajax tag (<p:ajax/>) inside the top level accordionPanel and this work."

Unfortunately that did not work (Tried every combination outer, inner, both + with & without activeIndex).

But this did

  • no need to set activeIndex, which is OK, because on the server side I'm not interested in the indexes):

  • in the outer:

    <p:ajax event="tabChange"/>
    

    (which basically is a restricted form of your solution

  • In the inner accordion I've added:

    <p:ajax event="tabChange" process="@this" update="@form"/>
    

    Meaning that in my case only a whole form rerender is necesarry on changing the inner tab status

Unfortunately this adds ugly flickering because of the complex page structure. I was hoping I could resolve it without ajax on tabchange.

Also found on primefaces forum

UPDATE 20130510

At the end I've ended up brewing my own solution. That is:

Having the constraint that we display the same number of accordions in the same order, we can save their status in a string containing 1 for every open and 0 for ever closed accordion. So the solution is as follows:

var CONTAINER_SELECTOR = 'selector of container containing all accordions';

// called whenever we click on an accordion (see assignment in init)
var saveAccordionsState = function () {
  var state = '';
  $(CONTAINER_SELECTOR + ' .ui-accordion-header').each(function (i, el) {
      // for every accordion put a 0 or 1 in the state string
      state += ($(el).hasClass('ui-state-active') ? '1' : '0');
  });
  // put the state string in an input that will be submitted to the server
  $('#accordionState').val(state); 
};

// this method is called right after accordions are rendered
var init = function () {
    // retrieve the saved state
    var state = $('#accordionState').val();

    // get reference to all accordions
    var $accordions = $('.ui-accordion-header');

    // turn jquery effects off for quick restoration (otherwise we'll have glitches
    $.fx.off = true;

    if (state === '' || $accordions.length !== state.length) { // if no state defined 
        // open all accordions by simulating a click on them
        $('.ui-accordion-header').not('.ui-state-active').trigger('click');
    } else { // otherwise
        for (var i = state.length - 1; i >= 0; --i) {
            var $accordion = $($accordions[i]);
            var c = state[i];
            // for every accordion that is in the opposite state
            if ((c === '1' && !$accordion.hasClass('ui-state-active')) ||
                (c === '0' && $accordion.hasClass('ui-state-active'))) {
                $accordion.trigger('click'); // simulate a click on its header to toggle it
            }
        }
    }
    // turn effects back on
    $.fx.off = false;
    // save the state
    saveAccordionsState();
    // assign the save state method to every accordion header click
    $('.ui-accordion-header', $featureTable).click(saveAccordionsState);
};

The xhtml looks something like this:

<h:form>
    <p:accordionPanel ... >
        <p:tab ... >
            <h:panelGroup ... >
                <p:accordionPanel ... >
                    <p:tab ... >
                        <h:panelGroup ... />
                    </p:tab>
                </p:accordionPanel>
            </h:panelGroup>
        </p:tab>
    </p:accordionPanel>
    <h:inputHidden id="accordionState" value="#{bean.accordionState}"/>
    <script type="text/javascript">
        init();
    </script>
</h:form>

Upvotes: 2

Related Questions