Cascading LOV in Tabular Form

Da Oracle leider noch keine out-of-the-box Lösung zu diesem Thema bereitstellt, möchte ich in diesem Beitrag ein Beispiel aufzeigen, wie man eine cascading LOV in einer Tabular Form realisieren kann.
Es gibt sicherlich noch andere Ansätze und Lösungen, aber diese erscheint mir derzeit die einfachste funktionierende zu sein.

Diese kleine Beispiel-Anwendung basiert auf dem EMP-Schema und der EMP-Tabelle. Per Definition sollen bei der Auswahl eines Departments nur die Personen als Manager angezeigt werden, die dem entspr. Department zugeordnet sind.
D.h. dass die Spalte MGR als cascading LOV realisiert werden soll. Durch Änderung des Departments soll sich die MGR-Liste enstp. des zuvor ausgewählten Departments automatisch aktualisieren.
Das Beispiel dazu finden Sie hier: http://apex.oracle.com/pls/apex/f?p=39514

Die nachfolgende Beschreibung basiert darauf, dass bereits eine Tabular Form existiert und in etwa so aussieht, wie in folgender Abbildung dargestellt. Die LOVs auf den Spalten DEPTNO und MGR sind simple LOVs, die alle Departments bzw. alle Personen selektieren.

Aufbau Tabular Form EMP

Welche Schritte sind nun zusätzlich notwendig, um eine cascading LOV in einer Tabular Form zu realisieren?
1. Hilfs-Item erzeugen
2. On-Demand Prozess erzeugen
3. Javascript Funktion mit AJAX-Call implementieren
4. Javascript Funktionsaufrufe integrieren

Lösung

 

1. Zuerst erstellen wir ein Hilfs-Item, mit dem wir die Parent-ID (in unserem Falle die Dept-No) austauschen können 

  • Create Page Item
  • Item Type: HIDDEN
  • Name: P<x>_DEPTNO_REFVAL („Px_“ bitte austauschen durch die entsprechende Page-Nummer)
  • Protected: NO
  • default belassen und „Create…“

2. On-Demand Prozess erzeugen

Dieser Prozess soll entspr. der ausgewählten Department-ID alle Employees lesen und als JSON-String aufbereiten.
Hinweis: Dieser Prozess wird später als Ajax-Call aufgerufen!

  • Shared components -> Application Processes -> Create
  • Name: GET_CASCADING_LOV_JSON (ACHTUNG! hier bitte unbedingt auf Groß-/ Kleinschreibung achten!)
        Sequence: <egal>
        Process Point: On Demand
  • Process Text:

BEGIN
  — hier bitte unbedingt beim item-namen das „x“ durch ihre page-nr ersetzen
  APEX_UTIL.JSON_FROM_SQL(’select ename, empno from emp where deptno = ‚||:P<x>_DEPTNO_REFVAL||‘ order by 1′);
END;

  • KEINE Conditions angeben und „Create Process“ klicken.

3. Javascript Funktion mit AJAX-Call implementieren

Der AJAX-Call wird vollständig in Javascript erstellt. Für dieses Beispiel werde ich den JS-Code einfach in die Form übernehmen.
Bitte gestatten Sie mir die Anmerkung, dass ich in Bezug auf die Widerverwendbarkeit und Lesbarkeit den JS-Codes lieber in einem separatesn File ablege und dieses JS-File über das script-Tag importiere.
Jedoch zur Vereinfachung wollen wir die JS-Funktion einfach in die Form reinkopieren.

JS-Code anpassen
    Damit unsere AJAX-Call später richtig funktioniert, müssen wir zuerst einige Anpassungen machen.
    Bitte tauschen Sie mit Suchen und Ersetzen …

  • „f05“ durch die item-nr des DEPTNO-items in ihrem report
  • „f06“ durch die item-nr des MGR-items in ihrem report
  • „P<x>_DEPTNO_REFVAL“ => das <x> ersetzen durch ihre page-nr.
  • „…APPLICATION_PROCESS=GET_CASCADING_LOV_JSON…“ => tauschen gegen den Namen des On-Demand Processes.
  • „myLovObjects.row[i].EMPNO“ und „myLovObjects.row[i].ENAME“ tauschen gegen die Spalten-Namen, die Sie im On-Demand Prozess verwendet haben.

     Bevor ich die einzelnen Codestellen erkläre, hier erstmal der gesamte JS-Code.

function refresh_cascading_lov(pRowItemObj){

  // separate given html-object, name and ID.
  var tmpRowObj = $(‚#’+pRowItemObj.id);
  var tmpObjName = tmpRowObj.attr(’name‘);
  var tmpObjID   = tmpRowObj.attr(‚id‘);
 
  // compute begin-position of row-number.
  var pos = tmpObjID.lastIndexOf(‚_‘);
  if(pos <= 0)
    return;
 
  var row = tmpObjID.substring(pos+1, tmpObjID.length);
  var rowNum = null;
  try {
    rowNum = parseInt(row);
  } catch (E) {
    rowNum = 1;
  }
 
  var selectTag = $(‚#f06_’+row);
  var selectTagVal = selectTag.val();
 
  // if curr-item is NOT the parent-id item, check if list would be refreshed for this row before.
  // to check this we have to create a help-item per row and call it „hMgrRefreshFlag_<rownum>“
  var mgrRefreshFlag = $(‚#hMgrRefreshFlag_’+row);
  var mgrRefreshFlagVal = mgrRefreshFlag.text();
  // if refresh-flag exists …
  if(mgrRefreshFlagVal!=undefined && mgrRefreshFlagVal!=null && mgrRefreshFlagVal!=““) {
    // EXISTS Path: check if the list had been refreshed before.
    if(mgrRefreshFlagVal==’true‘) {
      if(tmpObjName == „f05“) {
        null;
      } else {
        $(‚#f06_’+row).val(selectTagVal);
        return;
      }
    } else {
      // EXISTS Path: if it not had been refreshed before, set value to „true“ -> refreshed for this row!
      $(‚#hMgrRefreshFlag_’+row).text(‚true‘);
    }
  } else {
    // NOT-EXISTS Path: if not exists, create this help-item and set the value to „true“.
    tmpRowObj.after(‚<span id=“hMgrRefreshFlag_’+row+'“ name=“hMgrRefreshFlag“ style=“display:none;“>true</span/>‘);
  }
 
  /*
  set parent-id (deptno) to ref-item.
  !!! NOTE !!!
  In this case parent-id (deptno) is column f05 in row. If you try this, please check column deptno what column-id it is in your case.
  The easiest way to do that is to view the generated html-code and look for an select-tag with the value of our departments.
  */
  var deptObj  = $x(‚f05_’+row);
  var deptObjVal  = deptObj.value;
  var lovRefID = deptObjVal;
  $(‚#P6_DEPTNO_REFVAL‘).val(lovRefID);
 
  /*
  define the Ajax call. The only variable of note in this example is the application_process of type „On Demand“ created a view moments before.
  */
  var get = new htmldb_Get(null, $v(‚pFlowId‘), ‚APPLICATION_PROCESS=GET_CASCADING_LOV_JSON‘,$v(‚pFlowStepId‘));
 
  /*
  add the value in f05_xxxx into the session value for P6_DEPTNO_REFVAL. this is important as without this step the APEX server would not know
  what value the user had entered
  */
  get.add(‚P6_DEPTNO_REFVAL‘,lovRefID);
 
  /*
  call the ondemand process and accept the returning values
  */
  var gReturn = get.get();
  var myLovObjects = $u_eval(‚(‚ + gReturn + ‚)‘); 
 
  /*
  remove all options from select-list
  */
  selectTag.empty();
  /*
  add null-option for select-list
  */
  selectTag.append(‚<option value=““>&nbsp;</option>‘);

  /*
  iterate above all returned list-objects and add as options to select-tag.
  */
  var empnoFoundInList=false;
  for(i=0; i<myLovObjects.row.length;i++) {
    var tmpEmpno = myLovObjects.row[i].EMPNO;
    /*
    if current empno in listis equal to mgr-no selected before, set this as default selected value.
    */
    if(tmpEmpno == selectTagVal) {
      selectTag.append(‚<option selected=“selected“ value=“‚+tmpEmpno+'“>’+myLovObjects.row[i].ENAME+'</option>‘);
      empnoFoundInList = true;
    } else {
      selectTag.append(‚<option value=“‚+tmpEmpno+'“>’+myLovObjects.row[i].ENAME+'</option>‘);
    }
  }
 
  // if empno not found in list, add empno as additional option.
  if(!empnoFoundInList) {
    selectTag.append(‚<option selected=“selected“ value=“‚+selectTagVal+'“>’+selectTagVal+'</option>‘);
  }
 
  /*
  at least mark default value as selected.
  */
  var tmpF06=$(‚#f06_’+row);
  tmpF06.val(selectTagVal);
  var tmp2=$(‚#f06_’+row).val();
}

 

Nun ein paar wichtige Erklärungen zum JS-Code.

Der funktion „refresh_cascading_lov“ wird als Argument pRowItemObj übergeben. Hier steht später die Select-Liste als Objekt drin, auf das der Anwender geklickt hat (also entweder DEPTNO oder MGR).
Als erstes ermitteln wir nun den Namen und die ID des übergebenen Objekt und ziehen uns daraus die jeweilige Zeilennummer. Dann ermitteln wir noch die Ziel-Select-Liste und speichern den Ursprungswert.

  var tmpRowObj = $(‚#’+pRowItemObj.id);
  var tmpObjName = tmpRowObj.attr(’name‘);
  var tmpObjID   = tmpRowObj.attr(‚id‘);
 
  // compute begin-position of row-number.
  var pos = tmpObjID.lastIndexOf(‚_‘);
  if(pos <= 0)
    return;
 
  var row = tmpObjID.substring(pos+1, tmpObjID.length);
  var rowNum = null;
  try {
    rowNum = parseInt(row);
  } catch (E) {
    rowNum = 1;
  }

  var selectTag = $(‚#f06_’+row);
  var selectTagVal = selectTag.val();

Refresh-Flag einführen

Der folgende Code ist vielleicht auf den ersten Blick etwas verwirrend. Zum besseren Verständnis erstmal die Problematik, die wir damit lösen wollen.
Solange der Anwender später immer zuerst das Department und dann den Manager auswählt, haben wir kein Problem. Nun kann es aber sein, dass der Anwender OHNE zuvor das Department zu wechseln, direkt auf die Manager-Liste klickt. Beim ersten Mal stehen hier noch alle Werte drin, da diese noch nicht aktualisiert wurde. Später implementieren wir auf der MGR-Liste das Event „onFocus“, was diese Liste beim ersten Klick refreshed. Der folgende Code dient nun dazu zu verhindern, dass jedes Mal, wenn man auf die MGR-Liste klickt, der Server-Call ausgeführt wird. Beim ersten Mal reicht völlig aus.
Am besten wäre es, wenn man die Aktualisierung direkt beim Laden der Page durchführt. Nur leider sehe ich derzeit keine einfache Lösung, um dies auch so umzusetzen, dass es funktioniert. Vielleicht stellt Oracle irgendwann einmal Mechanismen zur Verfügung, mit denen man so etwas machen kann (ähnlich dem POST-QUERY-TRIGGER in Oracle Forms).

  var mgrRefreshFlag = $(‚#hMgrRefreshFlag_’+row);
  var mgrRefreshFlagVal = mgrRefreshFlag.text();
  // if refresh-flag exists …
  if(mgrRefreshFlagVal!=undefined && mgrRefreshFlagVal!=null && mgrRefreshFlagVal!=““) {
    // EXISTS Path: check if the list had been refreshed before.
    if(mgrRefreshFlagVal==’true‘) {
      if(tmpObjName == „f05“) {
        null;
      } else {
        $(‚#f06_’+row).val(selectTagVal);
        return;
      }
    } else {
      // EXISTS Path: if it not had been refreshed before, set value to „true“ -> refreshed for this row!
      $(‚#hMgrRefreshFlag_’+row).text(‚true‘);
    }
  } else {
    // NOT-EXISTS Path: if not exists, create this help-item and set the value to „true“.
    tmpRowObj.after(‚<span id=“hMgrRefreshFlag_’+row+'“ name=“hMgrRefreshFlag“ style=“display:none;“>true</span/>‘);
  }

Hier wird nun der Referenzwert, also die DEPTNO, aus der entspr. Zeile ermittelt und in das Hilfs-Item geschrieben. 
BEACHTE! Das Hilfs-Item haben wir im On-Demand Prozess angegeben.

  var deptObj  = $x(‚f05_’+row);
  var deptObjVal  = deptObj.value;
  var lovRefID = deptObjVal;
  $(‚#P6_DEPTNO_REFVAL‘).val(lovRefID);

Nun machen wir den AJAX-Aufruf, um die Aktualisierte Liste zu erhalten und speichern die Response, also unseren JSON-String, in „gReturn“.

  var get = new htmldb_Get(null, $v(‚pFlowId‘), ‚APPLICATION_PROCESS=GET_CASCADING_LOV_JSON‘,$v(‚pFlowStepId‘));
 
  get.add(‚P6_DEPTNO_REFVAL‘,lovRefID);
 
  var gReturn = get.get();

Der folgende Aufruf splitet nun den Return-String auf und erstellt ein neues JS-Objekt mit einem JS-Array. Darüber können wir dann auf die neuen LOV-Werte zugreifen.

  var myLovObjects = $u_eval(‚(‚ + gReturn + ‚)‘); 

Nun leeren wir die MGR-Liste (also das SELECT-Tag, indem wir alle OPTIONS entfernen) und erstellen Sie komplett neu. Dazu iterieren wir über alle LOV-Werte aus „myLovObjects“ und erstellen zu jedem ein neues OPTION-Tag.
Zusätzlich prüfen wir noch, ob die zuvor enthaltene Manager-ID im Array vorhanden ist und, wenn ja, setzen wir diese als „selected-option“. Wenn die Manager-ID nicht enthalten war, fügen wir nach der Iteration noch eine zusätzliche Option ein, die nur die nicht-enthaltene ID darstellt. Das ist jedoch Geschmacksache und kann verändert werden.

  selectTag.empty();
  selectTag.append(‚<option value=““>&nbsp;</option>‘);

  var empnoFoundInList=false;
  for(i=0; i<myLovObjects.row.length;i++) {
    var tmpEmpno = myLovObjects.row[i].EMPNO;
    if(tmpEmpno == selectTagVal) {
      selectTag.append(‚<option selected=“selected“ value=“‚+tmpEmpno+'“>’+myLovObjects.row[i].ENAME+'</option>‘);
      empnoFoundInList = true;
    } else {
      selectTag.append(‚<option value=“‚+tmpEmpno+'“>’+myLovObjects.row[i].ENAME+'</option>‘);
    }
  }
 
  if(!empnoFoundInList) {
    selectTag.append(‚<option selected=“selected“ value=“‚+selectTagVal+'“>’+selectTagVal+'</option>‘);
  }

Als letztes wollen wir noch, dass der Ursprungswert, wenn er in der neuen Liste enthalten ist, auch wieder selektiert wird. Hier könnte man
aber auch einfach die Liste auf den NULL-Wert setzen -> das ist auch Geschmacksache.

  var tmpF06=$(‚#f06_’+row);
  tmpF06.val(selectTagVal);
  var tmp2=$(‚#f06_’+row).val();

Nachdem wir nun den JS-Code angepasst haben, kopieren wir diesen nun einfach in die Page
    => Page Attributes aufrufen über Edit Page Attributes und nach unten scrollen bis „Function and Global Variable Declaration“
    => hier kopieren wir nun den zuvor angepassten JS-Code rein und klicken auf „Apply Changes“.

4: Javascript Funktionsaufrufe integrieren
Im Report müssen wir nun bei den spalten DEPTNO und MGR jeweils eine JS-Aufruf machen. Dazu die Report-Attributes aufrufen und

  • bei Spalte DEPTNO unter „Column Attributes“ -> „Element Attributes“ folgende onChange Funktion einbauen:
    • onchange=“javascript:refresh_cascading_lov(this);“
  • bei Spalte MGR unter „Column Attributes“ -> „Element Attributes“ folgende onChange Funktion einbauen:
    • onfocus=“javascript:refresh_cascading_lov(this);“

Erklärung:

  1. Der Aufruf bei DEPTNO sorgt dafür, dass die LOV auf der Spalte MGR aktualisiert wird, wenn sich die DEPTNO ändert.
  2. Der Aufruf bei MGR sorgt dafür, dass wenn man auf die select-list bei MGR klickt, ohne dass zuvor die DEPTNO geändert wurde, die aktuelle LOV entspr. der angezeigten DEPTNO aufgerufen wird.

HINWEIS: Dieser Aufruf sollte eigentlich beim onload-Event für jede Zeile stattfinden. Leider kenne ich derzeit keine Möglichkeit, wie ich APEX dazu bewegen kann, ohne die Tabular Form komplett von Hand zu rendern.

Viel Glück!

8 Antworten auf „Cascading LOV in Tabular Form“

  1. Hallo,

    ich habe deinen Code ausprobiert.
    Habe alles so getan, wie du es beschrieben hast.
    Klappt bei mir leider nicht.
    Ich habe APEX 4.2.1.00.08 und FireFox 30.0
    Ich habe in den JS Code und in den Demand-Prozess jeweils Befehl gesetzt, dass ein anderes Feld ein Wert bekommt.
    Beide werden nich ausgelöst.
    Ich vermute, dass der OnChange gar nicht los läuft.
    Der steht im Tabular Form, unter der richtigen Spalte unter Element Attributes.
    Hast du da noch eine Idee woran es liegen kann?

    Danke Rafael

      1. Moin,
        Dein Beispiel läft bei mir.
        Ich kann es aber nicht nachbauen.
        Mein onchange startet nicht.
        Ich habe ein kleines Testszenario entworfen, dass mein onchange nur eine Error-Fenster öffnet.
        Mit der Verwendung eines Buttons gibt es keine Probleme.
        Dann habe ich Tabular Form angelegt und unter Report Attributes/Spalte/Column Atrributes/Element Attributes onchange=”javascript:onchange1(this);“ eingesetzt.
        Klappt aber nicht.
        Kann das an JQuery / UI liegen?
        Nochmals Danke im Voraus

        Rafael

  2. Moin,

    ich habe einen Teil geschafft.
    Deine Hochkomma und Anführungsstriche werden zu irgendetwas anderem bei mir, wenn ich sie kopiere.
    Nachdem ich die ersetzt habe, klappt der onchange schon.
    Nun muss ich deinen Code noch durchgehen und dort auch alles ersetzen.
    Danke

    Rafael

  3. Hallo, mein deutschen Grammatik nicht gut. Kann jemand bitte erklären, in englischer Sprache, wo die Ajax-Aufruf gesetzt und erfrischen Flagge? Ich verstehe nicht, wo man den Code, der mit „mgrRefreshFlag var“ beginnt gestellt. Bitte erläutern Sie, danke.

    (Hello, my German grammar is not good. Can someone please explain in English where to put the Ajax call and refresh flag? I do not understand where to put the code that begins with „mgrRefreshFlag var“. Please explain, thanks.)

    1. Hi Saphira,
      sorry for the delay.
      OK, the „mgrRefreshFlag“ is a HTML span-Tag which will only be added automatically by the javascript code (please have a look at the „NOT-EXISTS Path“ of the if-condition).
      And the Ajax call is also integrated in the javascript code

      //
      // define the Ajax call. The only variable of note in this example is the application_process of type "On Demand" created a view moments before.
      //
      var get = new htmldb_Get(null, $v('pFlowId'), 'APPLICATION_PROCESS=GET_CASCADING_LOV_JSON',$v('pFlowStepId'));

      Please have a look at the javascript function start with: function refresh_cascading_lov(pRowItemObj){ …
      This javascript function has to be adapted to your coding and then be stored in apex (at the page property in Javascript region or as a JS-File and referenced by your page).
      You can see the final integration at http://apex.oracle.com/pls/apex/f?p=39514

Schreibe einen Kommentar