0

Primefaces 6.0 (community edition); Mojarra: 2.2.8-18; Glassfish: 4.1; Java version: 1.8.0_102; jQuery

[EDIT: 2016-10-05 An answer using pure Primefaces+JSF for the following general question has now been self-answered (see detailed answer below):

Q1: How link to and target/open a p:tab within an p:accordionPanel within a p:tab within a p:tabview ?

The remainder of the descriptions of here below concern failed attempts at achieving the same end by using JavaScript pseudo-clicking on an outer p:tab (within a p:tabView) and then an inner p:tab (within a p:accordionPanel). The challenge (now basically academic) is how to synchronise these when both the p:tabView and p:accordionPanel are dynamic and non-cached.]


Under @ViewScoped I have a inner p:tab within a dynamic, non-cached p:accordionPanel within an outer p:tab within a dynamic, non-cached p:tabView. I have to be able to leave that inner tab and return to it from other pages.

IMPORTANT: the problem only happens for dynamic p:tabView and p:accordionPanel, and any acceptable answer must work for dynamic mode, as the real web app requires dynamic because it uses an expert system and the real content shown in any innermost tab may be impacted on by data from elsewhere

<h:body>

<h:outputScript name="js/changeTab.js" />                    

<h:form id="form" prependId="false">  

<p:tabView id='tabview' dynamic="true" cache="false" widgetVar="widgetTabView">

 <p:tab title="Outer Tab1" id="tabOuter1">Content of Tab1</p:tab>

 <p:tab title="Outer Tab2" id="tabOuter2">

  <p:accordionPanel 
    id="accordion"
    activeIndex="null" 
    dynamic="true" 
    cache="false" 
    widgetVar="widgetAccordion"
                    >
    <p:tab title="Inner Tab1" id="tabInner1">Content of inner Tab1</p:tab>

    <p:tab title="Inner Tab2" id="tabInner2">Content of inner Tab2</p:tab>

  </p:accordionPanel>                    
 </p:tab>

</p:tabView>

<ui:param name="tabViewId" value="#{param['tabViewId']}"/>
<ui:param name="tabId" value="#{param['tabId']}"/>
<ui:param name="accordionId" value="#{param['accordionId']}"/>
<ui:param name="accordionTabId" value="#{param['accordionTabId']}"/>

I need the following to open up the innermost tab:

/faces/tabs_accordions.xhtml?tabViewId=tabview&tabId=tabOuter2&accordionId=accordion&accordionTabId=tabInner2

My JavaScript (included via changeTab.js), which imitates clicking on the outermost tab then the innermost accordion tab, does not catch the innermost accordionPanel+tab, the outermost tabView+panel has not made the targets available by the time I try to access the inner most parts.

I show one callback-based approach below first/ I have researched dozens of suggestions here for how to force consecutive synchronous functions, including using .done(), Promises, and many other approaches (see edited examples below), but all of them seem to fail for the same reason, something to with how Primefaces is handling the first click() on the outermost tabView/tab.

Example using basic callbacks: From changeTab.js:

function changeTabDynamicOuterCBwithParams(tabViewId, tabId, accordionId, accordionTabId, callback)
{

 // ... log params omitted ...

 $('#' + tabViewId + ' ul li a[href="#' + tabViewId + ':' + tabId + '"]').click();
 callback(tabViewId,accordionId,accordionTabId);    
}

/**
 * Simulates clicking, and thus selection, of a p:tab within a p:accordionPanel.
 * 
 * IMPORTANT: For h:form with prepend=false !
 * 
 * NB: There is usually a p:tabView, p:tab, p:accordionPanel, p:tab.
 * but the first p:tab does not affect the ultimate client-side id.
 * 
 * @param {type} tabViewId Identifier of an outer ancestor p:tabView
 * @param {type} accordionId Identifier of an immediate parent p:accordion
 * @param {type} tabId Identifier of an immediate child p:tab
 * @returns {undefined}
 */
function changeTabViewAccordionTabDynamic(tabViewId,accordionId, accordionTabId)
 {
    var i = 'changeTabViewAccordionTabDynamic';

    console.log(i+": tabViewId("+tabViewId+")");
    console.log(i+": accordionId("+accordionId+")");
    console.log(i+": accordionTabId("+accordionTabId+")");

    var id = tabViewId + ":"+accordionId+":"+accordionTabId;
    console.log(i+": id("+id+")");
    var div = $("[id=\'" + id + "'\]");
    console.log(div.length);
    // The id is on the DIV following the H3 to click on.
    console.log(i+": div.length("+div.length+")");
    var h3 = div.prev();
    console.log(i+": h3.length("+h3.length+")");

    console.log(i+": clicking header of inner tab within accordion within outer tab within tabview");
    h3.click();
}

Simplified for testing (without checking for existence of query params etc.) the following is in the page with the tabView/tab/accordionPanel/tab:

 changeTabDynamicOuterCBwithParams(
  '#{tabViewId}',
  '#{tabId}', 
  '#{accordionId}', 
  '#{accordionTabId}',
  changeTabViewAccordionTabDynamic
 );

The JS console gives:

changeTabDynamicOuterCB2: tabViewId(tabview)
changeTab.js:100 changeTabDynamicOuterCB2: tabId(tabOuter2)
changeTab.js:101 changeTabDynamicOuterCB2: accordionId(accordion)
changeTab.js:102 changeTabDynamicOuterCB2: accordionTabId(tabInner2)
changeTab.js:103 changeTabDynamicOuterCB2: clicking on outer tab within tabview THEN calling back on inner tab
changeTab.js:128 changeTabViewAccordionTabDynamic: tabViewId(tabview)
changeTab.js:129 changeTabViewAccordionTabDynamic: accordionId(accordion)
changeTab.js:130 changeTabViewAccordionTabDynamic: accordionTabId(tabInner2)
changeTab.js:133 changeTabViewAccordionTabDynamic: id(tabview:accordion:tabInner2)
changeTab.js:135 0
changeTab.js:137 changeTabViewAccordionTabDynamic: div.length(0)
changeTab.js:139 changeTabViewAccordionTabDynamic: h3.length(0)

Note how the DIV and H3 selection fail, because they are not yet available.

However, if I do it in the JavaScript console of a browser, after the first click() is finished and the tabs with inner accordion/tabview have loaded properly, this selection works fine:

id='tabview:accordion:tabInner2'
var div = $("[id=\'" + id + "'\]");
var h3 = div.prev();

Result:

 h3;
<h3 class=​"ui-accordion-header ui-helper-reset ui-state-default ui-corner-all" role=​"tab" aria-expanded=​"false" aria-selected=​"false" tabindex=​"0">​…​</h3>​

Using PrimeFaces widget vars also does not work for the same reason:

function changeAccordionTabPF(accordionWidget,index) {
 PF(accordionWidget).select(index);     
}

If I use this for the outer part first, then the inner part, the accordionWidget var is not yet available.

Q2: How can I target a click() on the innermost tab by making sure it is available using synchronisation ?

An acceptable answer to this question must answer it for this specific Primefaces tab situation; please do not just refer me to any of the other stackoverflow answers about executing asynchronous functions in order using callbacks, deferred/done/next, or Promises, I have already read and tried dozens of them.


[EDIT: some more examples that don't work because the targets of the selection for the innermost tab of the accordion (within a tab within a tabView) are not available when the 2nd click simulation runs. Some are adapted from: https://stackoverflow.com/questions/39717433/how-pass-parameters-to-downstream-function-with-done-or-next]

function changeTabDynamicOuterDeferred(tabViewId, tabId)
{
  $('#' + tabViewId + ' ul li a[href="#' + tabViewId + ':' + tabId + '"]').click();
  return $.Deferred().resolve();
}

function changeTabViewAccordionTabDynamic(tabViewId, accordionId, accordionTabId)
{
  var id = tabViewId + ":" + accordionId + ":" + accordionTabId;
  var div = $("[id=\'" + id + "'\]");
// The id is on the DIV following the H3 to click on.
  var h3 = div.prev();
  h3.click();
}

Called as:

changeTabDynamicOuterDeferred(
 '#{tabViewId}',
 '#{tabId}'
).done(
   function() {
     changeTabViewAccordionTabDynamic('#{tabViewId}','#{accordionId}','#{accordionTabId}')
   }
).fail(
  function(err) {
   console.log(err)
  }
);

Everything seems to work (and the values for the targeted id pass fine) EXCEPT that by the time changeTabViewAccordionTabDynamic the elements to be selected are not yet available (apparently not yet completely rendered from the click() in changeTabDynamicOuterDeferred), despite the usage of done(.

Same problem with this variation using a promise with then(:

function changeTabDynamicOuterDeferredPromise(tabViewId, tabId)
{
  $('#' + tabViewId + ' ul li a[href="#' + tabViewId + ':' + tabId + '"]').click();

  return $.Deferred(
        function (dfd) {
            dfd.resolve()
        }
  ).promise();
}

With changeTabViewAccordionTabDynamic as above and called as:

changeTabDynamicOuterDeferredPromise(
 '#{tabViewId}',
 '#{tabId}'
).then(
  function () {
    changeTabViewAccordionTabDynamic('#{tabViewId}','#{accordionId}','#{accordionTabId}')
  }
).fail(
  function(err) {
    console.log(err)
  }
);

EDIT: I tried putting a while loop into the 2nd function for imitating a click on the inner tab within the accordionPanel (within a tab within a tabView), in part so I can see better what is available when. The effect of the click() on the outer tab (within the top-level tabView) can't be seen until after the long loop completes, and thus the selection always fails:

function changeTabViewAccordionTabDynamic(tabViewId, accordionId, accordionTabId)
{
    var id = tabViewId + ":" + accordionId + ":" + accordionTabId;
    var timer = 0;
    var div = $("[id=\'" + id + "'\]");
    while (div.length < 1 && timer++ < 100000) {
      div = $("[id=\'" + id + "'\]"); // Proper code would make this DRY
      console.log(div.length);
    }
   // The id is on the DIV following the H3 to click on.
   var h3 = div.prev();
   h3.click();
}

This occurs no matter which synchronisation strategy I use from above. And similarly, the primefaces widgetVar of the inner accordion and tab are not available.


EDIT: More attempts that fail. The following is adapted from How to execute JavaScript after page load?:

jQuery(document).ready(function () {
  jQuery(document).ready(function () {
    changeTabDynamic('#{tabViewId}','#{tabId}');        
    jQuery(document).ready(function () {
      changeTabViewAccordionTabDynamic('#{tabViewId}','#{accordionId}','#{accordionTabId}');  
    });        
  });
});    

The changes due to changeTabDynamic('#{tabViewId}','#{tabId}') are still not available to the next function changeTabViewAccordionTabDynamic('#{tabViewId}','#{accordionId}','#{accordionTabId}') by the time it runs, so the selectors fail.

I tried variations on using p:remoteCommand, with the same problem:

<p:remoteCommand oncomplete="changeTabDynamic('#{tabViewId}','#{tabId}')" autoRun="true" async="false"/>

<p:remoteCommand oncomplete="changeTabViewAccordionTabDynamic('#{tabViewId}','#{accordionId}','#{accordionTabId}')" autoRun="true" async="false"/>

Same problem.

Community
  • 1
  • 1
  • Off-topic: never use ` : http://stackoverflow.com/questions/7415230/uiform-with-prependid-false-breaks-fajax-render (if I remember correctly, it breaks `p:ajax` to ` – Kukeltje Sep 27 '16 at 06:21
  • tried without `dynamic="true"`? – Kukeltje Sep 27 '16 at 06:23
  • @Kukeltje Thanks for suggestions (and your other frequent help on this forum). Re `prependId="false"`. Am aware of that weakness, but would require migration of a large web app; Is planned for future. Tried 'dynamic=false', and yes, the simple callback chaining approach then works perfectly, but that is not an option under any circumstances for my real world web app, which uses expert system technology and must update 100% accurately to reflect impact from other dependencies elsewhere in the sytem. Caching is also BTW therefore out. (See also question edits.) – Webel IT Australia - upvoter Sep 27 '16 at 06:32
  • What I would try is start with setting a semaphore and ids of the tabs that needs to be set when starting this action. I'd also add some ajax tags to the dynamic ones and in the oncomplete of these, call a javascript function that checks the values that were globally set and call widgetvars of nested tabs or mimic clicking on them. The 'oncompletes' are the callbacks you can use to check if something is finished. – Kukeltje Sep 27 '16 at 06:49
  • Any comment on my comment? You can edit your question multiple time, but if the issue stays the same, my previous comment on what I would try still stands. Please comment on that. – Kukeltje Sep 29 '16 at 06:19
  • @ Kukeltje "Any comment on my comment" Just very busy. Tried some other simpler things and reported on them first (including some suggestions from other people elsewhere) before will trying your more recent semaphore suggestions etc., which I appreciate. – Webel IT Australia - upvoter Sep 29 '16 at 09:57
  • I understand you are busy, but if there is no feedback on suggestions that might work, and continously other approaches are tried (all 'timing' related instead of explicit 'onComplete usage), the incentive to reply next time is less (I'm busy to, hope you understand) since the impression there is more interest in the other approaches. Hope you understand. Or at least comment something like 'thanks for the idea, will try that in a few days but am trying other approaches first since I think that.... ' ;-) like you did in your first comment ;-) – Kukeltje Sep 29 '16 at 10:03
  • You wrote " if there is no feedback on suggestions that might work, and continously other approaches are tried (all 'timing' related instead of explicit 'onComplete usage), the incentive to reply next time is less". I have in fact performed `oncomplete` tests (and even before you suggested them) I am not ready to report on them. I am fully aware that posting a stack question initiates a certain momentum, and that not folllowing up to the exclusion of all other activities (for whatever professional or personal reasons) runs the risk of missing the momentum of responders trying to help. Thanks. – Webel IT Australia - upvoter Oct 02 '16 at 06:32
  • A working solution using pure Primefaces+JSF via `activeIndex` (without any JavaScript pseudo-clicking tricks or synchronisation challenges) has now been found, see self-answer below. The rest of my descriptions in my question and comments concerning failed attempts at synchronising JavaScript clicking of an outer p:tab then an inner p:tab are now considered academic (and are now low priority for me), but any feedback on why they failed (specifically with p:tabView+p:accordionPanel in dynamic non-cached mode) is still welcome. Thanks for your input. – Webel IT Australia - upvoter Oct 05 '16 at 11:23

1 Answers1

0

A working solution using pure Primefaces+JSF via activeIndex (without any JavaScript pseudo-clicking tricks) has now been found, see below. The rest of the descriptions above concerning failed attempts at synchronising JavaScript clicking of an outer p:tab then an inner p:tab are now considered academic (and are now low priority for me), but any feedback on why they failed is still welcome.


The following using activeIndex in an outer p:tabView then the inner p:accordionPanel works fine with dynamic non-cached.

One of the reasons I was persisting with (but have for now abandoned) attempts to imitate clicks on outer then inner tabs using id or widgetVar was so that I could avoid hard-coded activeIndex numbers (so that the solution would be robust against inserting new tabs, because each target id or widgetVar is stable); it turns out if one uses f:viewParam parameters bound to a navigation bean throughout one does not have to hard-code any activeIndex values.

In the page tabs_accordions.xhtml with the p:tabView/p:tab/p:accordionPanel/p:tab nesting:

<f:view>
  <f:metadata>                
   <f:viewParam name="tabViewActiveIndex" value="#{navBean.tabViewActiveIndex}" required="false"/>
   <f:viewParam name="accordionActiveIndex" value="#{navBean.accordionActiveIndex}" required="false"/>
...                                
 </f:metadata>
</f:view>

<p:tabView 
 id='tabview' 
 dynamic="true" 
 cache="false" 
 widgetVar="widgetTabView"
 activeIndex="#{navBean.tabViewActiveIndex}"
>

 <p:tab title="Outer Tab1" id="tabOuter1">
   Content of Tab1
 </p:tab>

 <p:tab title="Outer Tab2" id="tabOuter2" >

  <p:accordionPanel 
    id="accordion"
    dynamic="true" 
    cache="false" 
    widgetVar="widgetAccordion"
    activeIndex="#{navBean.accordionActiveIndex}" 
    >

     <p:tab title="Inner Tab1" id="tabInner1">
       <h:link 
         outcome="dummy_edit_viewParam" 
         value="Link1: Go to pretend edit page then return to this 1st inner tab">
          <f:param name="stem" value="tabs_accordions"/>
          <f:param name="tabViewActiveIndex" value="#{navBean.tabViewActiveIndex}"/>
          <f:param name="accordionActiveIndex" value="#{navBean.accordionActiveIndex}"/>
         </h:link>                            
      </p:tab>

     <p:tab title="Inner Tab2 " id="tabInner2">
         <h:link 
            outcome="dummy_edit_viewParam"
            value="Link2: Go to pretend edit page then return to this 2nd inner tab">
            <f:param name="stem" value="tabs_accordions"/>
            <f:param name="tabViewActiveIndex" value="#{navBean.tabViewActiveIndex}"/>
            <f:param name="accordionActiveIndex" value="#{navBean.accordionActiveIndex}"/>
          </h:link>                                                        
      </p:tab>

   </p:accordionPanel>
 </p:tab>
</p:tabView>

The links (from either inner tab) go to dummy_edit_viewParam.xhtml, which has:

<f:view>
 <f:metadata>
   <f:viewParam name="stem" value="#{navBean.stem}" required="true"/>
   <f:viewParam name="tabViewActiveIndex" value="#{navBean.tabViewActiveIndex}" required="true"/>
   <f:viewParam name="accordionActiveIndex" value="#{navBean.accordionActiveIndex}" required="true"/>
  </f:metadata>
</f:view>

Then on (for example) saving after some edits, one can return to whatever inner tab one came from.

The interesting bit is that in p:tabView and p:accordionPanel this not only opens a desired tab, it also sets the corresponding value in the navigation bean when a tab is clicked (i.e. opening a tab other than the tab targeted by the initial parameters):

activeIndex="#{navBean.tabViewActiveIndex}" 

activeIndex="#{navBean.accordionActiveIndex}" 

For example, if you enter the view cycle with this it opens the 1st inner tab of the 2nd outer tab:

/faces/tabs_accordions.xhtml?tabViewActiveIndex=1&accordionActiveIndex=0

If you then click on the 2nd inner tab of the 2nd outer tab it will set #{navBean.accordionActiveIndex} to 1 corresponding to that tab !

So when one follows this link (from within the 2nd inner tab of the 2nd outer tab) this will send info to target the correct tab:

<p:tab title="Inner Tab2 " id="tabInner2">
  <h:link 
    outcome="dummy_edit_viewParam"
    value="Link2: Go to pretend edit page then return to this 2nd inner tab">
    <f:param name="stem" value="tabs_accordions"/>
    <f:param name="tabViewActiveIndex" value="#{navBean.tabViewActiveIndex}"/>
    <f:param name="accordionActiveIndex" value="#{navBean.accordionActiveIndex}"/>
  </h:link>                                                        
</p:tab>

When these params are used to eventually return to the original tabView/accordionPanel (such as after editing values in another page and returning on save) it will now target that 2nd inner tab automatically:

/faces/tabs_accordions.xhtml?tabViewActiveIndex=1&accordionActiveIndex=1

For example, the action of a save button for a page to return from might be:

public String saveReturnToTabViewAccordionUseParams() {    
  return "tabs_accordions?faces-redirect=true&includeViewParams=true";
}    

And all of this then works without any hard-coding of each target activeIndex.