Wednesday, January 23, 2013

Targetting elements in a wizard-generated tabular form

This piece of code has appeared before, but i wish to revisit it after having used it again, and how convenient is was for me.
Right of the bat: this will only work with wizard generated tabular forms and not with manual tabular forms.
My goal was to lessen dependance on the f## arrays in the tabular forms, which especially in javascript can be outright frustrating. Until i used this code, i usually declared some global params and commented a lot about how each array maps to which column. Only to then afterward having to switch the column around with another one, or add one in, and find the code just doesn't work anymore because of the f-arrays.
var gEmpno = 'f01',
    gEname = 'f05',
    gSal   = 'f06';
$('input[name="f04"][type="text"]:visible')
Oh no. A column had to be added between 3 and 4! Renumber all javascript code.:-( Switched column order in the report attributes? Renumber all code :-(
My code works with the mapping which apex generates when it renders a tabular form. As an example, i have tabular form based on EMP:
select 
"EMPNO",
"EMPNO" EMPNO_DISPLAY,
"ENAME",
"JOB",
"MGR",
"HIREDATE",
"SAL",
"COMM",
"DEPTNO"
from "#OWNER#"."EMP"
This the query generated by apex when selecting the EMP table as source and indicating EMPNO is the primary key (no rowid used).
I only altered column DEPTNO to be a select list instead of a textfield, so it would show the actual department names and so i would have select list in my tabular form.
When the page is runned, then below the tabular form HTML an additional mapping will be rendered:
<input type="hidden" id="fmap_001" value="EMPNO" name="fmap">
<input type="hidden" id="fhdr_001" value="Empno" name="fhdr">
<input type="hidden" id="fmap_002" value="ENAME" name="fmap">
<input type="hidden" id="fhdr_002" value="Ename" name="fhdr">
<input type="hidden" id="fmap_003" value="JOB" name="fmap">
<input type="hidden" id="fhdr_003" value="Job" name="fhdr">
<input type="hidden" id="fmap_004" value="MGR" name="fmap">
<input type="hidden" id="fhdr_004" value="Mgr" name="fhdr">
<input type="hidden" id="fmap_005" value="HIREDATE" name="fmap">
<input type="hidden" id="fhdr_005" value="Hiredate" name="fhdr">
<input type="hidden" id="fmap_006" value="SAL" name="fmap">
<input type="hidden" id="fhdr_006" value="Sal" name="fhdr">
<input type="hidden" id="fmap_007" value="COMM" name="fmap">
<input type="hidden" id="fhdr_007" value="Comm" name="fhdr">
<input type="hidden" id="fmap_008" value="DEPTNO" name="fmap">
<input type="hidden" id="fhdr_008" value="Deptno" name="fhdr">
This is what i based my mapping on. I only look at the fmap array. And all items are always in the same order as they are in the report attributes. This order also determines which f##-array has to be assigned. A hidden column for example could be the last element in the mapping and thus be f09, while the inputs for array f09 are rendered in the same column as the inputs for array f05.
I've tested through playing with elements in the tabular form, hide them, reorder them, remove and add. The mapping would always reflect the actual correct values.

Place this code in the javascript section of the page:
var gaInputMapping = new Array();
$().ready(function(){
   apex.debug('Initializing input mapping array...');
   $("input[name='fmap']").each(function(index){ 
      var lsHeader = $(this).val(),
          lsName = 'f'+('0'+(index+1)).slice(-2),
          lsNName = $("[name='"+lsName+"']")[0].nodeName,
          lMap = {"header":lsHeader,"name":lsName,"nodeName":lsNName};
      apex.debug('Header: '+lsHeader+' - Array name: '+lsName+' - Node type: '+lsNName);
      gaInputMapping.push(lMap);
   });
});
function getNameWithHeader(pHeader){
   var lsName;
   $.each(gaInputMapping, function(index){
      if(gaInputMapping[index].header==pHeader.toUpperCase()){
         lsName= gaInputMapping[index].name;
      };
   });
   return lsName;
};
function getHeaderWithName(pName){
   var lsHeader;
   $.each(gaInputMapping, function(index){
      if(gaInputMapping[index].name.toUpperCase()==pName.toUpperCase()){
         lsHeader= gaInputMapping[index].header;
      };
   });
   return lsHeader;
};
function getMapEntry(pHeader){
   var lRet;
   $.each(gaInputMapping, function(index){
      if(gaInputMapping[index].header==pHeader.toUpperCase()){
         lRet= gaInputMapping[index];
      };
   });
   return lRet;
};
function getSelector(pHeader){
   var lsSel;
   $.each(gaInputMapping, function(index){
      if(gaInputMapping[index].header==pHeader.toUpperCase()){
         lsSel= gaInputMapping[index].nodeName + "[name='" + gaInputMapping[index].name + "']";
      };
   });
   return lsSel;
};
function getObjectInSameRow(pHeaderFind, pCurrentItem){
   return $(getSelector(pHeaderFind), $(pCurrentItem).closest("tr"));
};
With these, i've executed some lines in the Firebug console:
>>> gaInputMapping
[Object { header="EMPNO", name="f01", nodeName="INPUT"}, Object { header="ENAME", name="f02", nodeName="INPUT"}, Object { header="JOB", name="f03", nodeName="INPUT"}, Object { header="MGR", name="f04", nodeName="INPUT"}, Object { header="HIREDATE", name="f05", nodeName="INPUT"}, Object { header="SAL", name="f06", nodeName="INPUT"}, Object { header="COMM", name="f07", nodeName="INPUT"}, Object { header="DEPTNO", name="f08", nodeName="SELECT"}]
>>> getNameWithHeader("EMPNO")
"f01"
>>> getNameWithHeader("MGR")
"f04"
>>> getHeaderWithName("f03")
"JOB"
>>> getMapEntry("SAL")
Object { header="SAL", name="f06", nodeName="INPUT"}
>>> getMapEntry("DEPTNO")
Object { header="DEPTNO", name="f08", nodeName="SELECT"}
>>> getSelector("HIREDATE")
"INPUT[name='f05']"
>>> getSelector("DEPTNO")
"SELECT[name='f08']"

$(getSelector("SAL"),"#rEmp").change(function(){
var lRow    = $(this).closest("tr"),
    lSal    = $(this).val(),
    lComm   = $(getSelector("COMM"), lRow).val(), 
    lEmpno  = $(getSelector("EMPNO"), lRow).val(), 
    lDeptno = $(getSelector("DEPTNO"), lRow).val();

console.log(lEmpno+' - '+lDeptno+' - '+lSal+' - '+lComm);
});

7369 - 20 - 1804 - 5 (after changing the first row sal value from 1803 to 1804)
Another example: get the EMPNO item on the same row as the current item. The getObjectInSameRow function is just a wrap-up of a frequently used technique to target an item on the same row as the current item, and allows you to do so via headers. There is no need to play with rowids and substrings.
$(getSelector("SAL"),"#rEmp").each(function(){
   console.log(getObjectInSameRow('EMPNO', $(this)));
});
This system is a lot more robust to me. I can now safely use the headers and don't have to use a coded f##-array anywhere anymore. I can switch columns and adjust their properties.
There are limits of course: removing columns while functionality relies on them would give you errors, obviously. Changing an item so no input/select is generated anymore, but display only will also break your functionality, as a non-submitting item will not generate an array. In short though, using headers is more intuitive and makes the code more maintainable, and that's all there really is to it.

Monday, January 7, 2013

Working with the tree in Apex

The tree in Apex is based on the jsTree plugin and is version 0.9.9a2 for apex version 4.0, 4.1 and 4.2.
There are quite often questions about how to work with the tree and perform some basic actions on it. Perhaps some folks just don't know that there actually is documentation available and where it resides. You can find the documentation in your apex_images folder under libraries\jquery-jstree\0.9.9a2.
Of course, if you're no javascript or jQuery wiz, you might find the documentation quite confusing. Even if you are, it isn't all that clear at first and i too struggled a bit initially with no examples available.
Before looking at the below questions i want to start off with this: all of the functions that are described in the documentation require a tree instance to invoke them. The methods are not static ones. Getting a tree instance:
Note that you could have more than one tree on a page and therefor i would advice to provide a static id to the tree region(s).

How to get a tree reference

Documentation: This functions returns a specific tree instance by an ID or contained node.
This means that we need an actual element that resides in the tree. Since the markup generated for the tree is always the same, and the top level element is a div with class tree, this element can easily be fetched.
var l$Tree = $("#myStaticRegionId div.tree");
With this element the tree reference can be retrieved
$.tree.reference(l$Tree)
This reference will be required for all actions performed on this tree.

Common questions

  • I want to search the tree

    $.tree.reference(l$Tree).search("text_youre_looking_for");
    Oh, but what does this do? It doesn't return anything? That is correct.
    From the documentation: The function triggers the onsearch callback, with the nodes found as a parameter.
    And under callback.onsearch: Default is:
    function(NODES, TREE_OBJ) { NODES.addClass("search"); }

    So, nodes which contain the text searched for will have a class "search" appended to them.
    To retrieve those nodes, use
    $(".search")
    Now, if you're using a tool such as Firebug, you might notice that a POST is being made to the server. It will fail however, each and every time. This is because for some reason the defined defaults have not actually been set in the tree initialisation. You can fix this by performing this:
    $.tree.reference(l$Tree).settings.data.async = false;
  • I want to get a node's text

    $.tree.reference(l$Tree).get_text()
  • I want search to do something different than assign a "search" class

    By default the "search" class will be set on the anchor tag of the list element. You could alter the behaviour by overriding the default onsearch function.
    $.tree.reference(l$Tree).settings.callback.onsearch = function(NODES, TREE_OBJ){
      $.each(NODES, function(){
        console.log("Matched node: " + this);
        $(this).addClass("search2")
      });
    };
    
    I'd show how to rename a node, but by default the rename won't work as it has been disabled (for some reason). You could do this by altering the .text() of the retrieved element, but that will clear out the ins-element. So if you go that route, append an ins element and then some text.
  • I want to do something when a node is selected

    $.tree.reference(l$Tree).settings.callback.onselect = function(NODE, TREE_OBJ){
    console.log("Selected node: " + NODE);
    };
    
    Don't use a .click event handler on the list items as clicks are fired for the element and its parent elements. If you want to use a click handler for each indivual element, you can use a selector like this, which would select the first anchor tag in each list element:
    $("#static_id div.tree li>a")
  • I want to get the currently selected node

    $.tree.reference(l$Tree).selected
  • I want to set a node as being selected (without clicking the node)

    $.tree.reference(l$Tree).select_branch(node, false);
    Where "node" would be either a tree node reference, dom element or jQuery object. The second argument is whether to multiselect or not.

    Small example on a tree based on EMP:
    $.tree.reference(l$Tree).search("KING");
    $.tree.reference(l$Tree).select_branch($(".search"), false);
    
  • I want to do something when a node is double-clicked

    $.tree.reference(l$Tree).settings.callback.ondblclk= function(NODE, TREE_OBJ) { 
    console.log("Double clicked node: " + NODE);
    };
    
  • I want to open or close a node/branch

    var lReturn = $.tree.reference(l$Tree).open_branch(node);
    var lReturn = $.tree.reference(l$Tree).close_branch(node);
    
    Where "node" would be either a tree node reference, dom element or jQuery object.
  • I want to do something when a node is opened or closed

    $.tree.reference(l$Tree).settings.callback.onopen= function(NODE, TREE_OBJ) { 
    console.log("Opened node: " + NODE);
    };
    
    $.tree.reference(l$Tree).settings.callback.onclose= function(NODE, TREE_OBJ) { 
    console.log("Closed node: " + NODE);
    };
    
These examples should provide you with enough knowledge about how to use the possible functions and callbacks of the tree.
You can also take a look at this small example i set up on apex.oracle.com!