Showing posts with label tabular-form. Show all posts
Showing posts with label tabular-form. Show all posts

Tuesday, March 19, 2013

Tabular form: select items on the same row

Introduction

Frequently I see people struggle with targetting things in a tabular form. While in a previous post i showed how to target some specific columns and find out how they relate to the fnn-arrays, I'd like to shed some light on how items on the same row of one can be targetted.
Some cherry picking:
https://forums.oracle.com/forums/thread.jspa?threadID=2479820
https://forums.oracle.com/forums/thread.jspa?threadID=2164344
https://forums.oracle.com/forums/thread.jspa?threadID=1117800
And there are plenty more of course.

The usual solution I see pass by is to target items by using their ID. I won't argue, this a valid method. The actual item's id is taken, and then the rowid suffix is extracted. This suffix then is concatenated with the desired item's array name, and an id targetting that item is constructed as such.
I don't like that. It has some shortcomings that are regulary forgotten or ignored, and these are called the fnn-arrays. But more on that further on.

Firstly I'd like to highlight another issue which is apparent: people do no understand, or even bother to look at, the html code and how to work it.

My setup

My example setup is still the same as in my previous post.
Tabular form with source sql:
select 
"EMPNO",
"EMPNO" EMPNO_DISPLAY,
"ENAME",
"HIREDATE",
"SAL",
"DEPTNO",
NULL checkme
from "#OWNER#"."EMP"
  • EMPNO: hidden
  • ENAME: popup lov, with query based lov:
    SELECT ename d, ename r FROM emp ORDER BY ename;
  • HIREDATE: date field
  • DEPTNO: select list based on a query:
    SELECT dname d, deptno r FROM dept ORDER BY dname;
  • CHECKME: simple checkbox, with values "Y,"
(This form isn't meant to do anything but serve a demonstration purpose.)
Once again I'll use Firefox + Firebug, and this is what you'll see in the screenshots. It's my most valued and appreciated tool! If you're unfamiliar with it, take a look at my previous post.

Concept

What I'm going to show here is how to work in row 3, and specifically starting from the item in the SAL column.
overview
Starting off with the concept of a table. Tables are always perceived by as a simply x*y-grid of cells, with x rows and y columns. Perhaps the most simple example is an Excel spreadsheet: say I want to target cell B2. B2 is on row 2:
excel - row
And is also in column B:
excel - column
The intersection of row 2 with column B is cell B2:
excel - cell
Of course, once an HTML table is displayed the principle is the same. To manipulate or retrieve from a table requires some knowledge of the HTML generated. So this is what you get when selecting the TBODY of the table:
tbody
As you can see, this element contains TR tags which are rows. The rows in turn contain TD elements:
tr - row
As is evident, there is no such thing as a column element. The tables are cells in rows.
td - cell
Luckily for us there is the headers attribute on the cells. With this header it is possible to target all cells with a given headers attribute, effectively providing a way to target all cells in a column.
Aside from that, this shows up that all td elements have a tr element as a parent, and the tr element in turn a tbody. So, targetting an item which is in the same row as another item should not prove to hard once you understand this structure.

Targetting on same row by substringing.

I want to select the input item in the SAL column on the third row. This is for demonstrative purpose. ":eq(2)" will target the 3rd item in the array of objects matching the selector (3rd, because of zero-index based arrays).
$("td[headers='SAL'] input:visible:eq(2)")
Executing this in the Firebug console will put 2 lines out to the console: one with the executed command and one with the result. You can hover over the result and it will show the actual element in the html.
input SAL - selected
When clicked you will be taken to the HTML tab in firebug, and the item will be selected in the structure.
td input SAL
You can see that the input item has been highlighted. This item is contained within a table cell element, and has 1 adjacent element in the form of a label element.
Take note of the item's id. This is what most people focus on when they work in a tabular form. It consists of the name attribute of the input element (and thus the associated fnn-array), and suffixed by the "rowid".
Most stop looking there. The rowid! THE ROWID!
$("input[name='f04_0003']")
td input HIREDATE
And now these inputs in the context of their row: inputs in row
So suppose that an event handler is bound to the input items in the SAL column, on change for example. When the change triggers, something has to be done in another column, on the same row.
To emulate this, I'll just select the input on the third row. (You could compare this to using $(this) in an actual event handler).
You'd then take the ID, and substring the rowid suffix. This can then in turn be easily suffixed to another array's name. For example, f04.
var rowid = $("td[headers='SAL'] input:visible:eq(2)").attr("id").substr(3);
console.log(rowid);
$("#f04"+rowid+"")
select by id substr
Another method would be to use replace on the id of the triggering item. This again requires you to know array names, of both the triggering item and the item you want to affect.
>>>> $("td[headers='SAL'] input:visible:eq(2)").attr("id").replace("f05","f04")
"f04_0003"

Considerations

Now I won't say that this method is bad or wrong. Obviously it works and if you're happy than that is fine by me. I however do not like it. Having to know which items map to which array is just a recipe for trouble to happen sooner or later. The problem is that the arrays can be switched so easily: a simple reordering of the columns or removing a column in the tabular form will outright break your javascript code when you used the arrays to target.
For example, say we have EMPNO and ENAME and both are editable. EMPNO is in array f01 and ENAME in f02. When you reorder these and ENAME comes before EMPNO, then ENAME will now be f01 and EMPNO will be f02. Now you will have to check your javascript code, and change it accordingly.
This is hellish if your tabular form has more than a few editable columns though. If it has 9 columns and 7 are editable and you have to make some change to it, like adding in a column in the fourth position, then you're out of luck. You will have at least 7 arrays to check up, and if your javascript is not littered with comments pointing out which array maps to which column, you will have to find out all over again.
And what if you have to revisit your code or form later on? Or the form? Or maybe not you, but a colleague? Oops. Chances are big that something will break.

Target by column headers

It really doesn't have to however, if you would target items not by array or rowid, but simply by using your knowledge of the html structure and employing some jQuery-fu. As I've shown above, all cells in a column have a headers attribute, and we can target a column by using this knowledge.
The input item has a cell as a parent. The cell has a row as parent. The row has the table body as parent.
For example, input item in column SAL, on row 3. The parent row element can then be retrieved by using ".closest"
$("td[headers='SAL'] input:visible:eq(2)").closest("tr")
Then from this row element we can traverse down again. Find the cell with headers=HIREDATE and then select the visible input in that column. All chained together:
$("td[headers='SAL'] input:visible:eq(2)").closest("tr").find("td[headers='HIREDATE'] input:visible")
select by traverse Finding multiple items starting from one:
By traversing the dom:
var parentRow = $("td[headers='SAL'] input:visible:eq(2)").closest("tr");
console.log(parentRow.find("td[headers='HIREDATE'] input:visible"));
console.log(parentRow.find("td[headers='ENAME'] input:visible"));
console.log(parentRow.find("td[headers='DEPTNO'] select:visible"));
select multiple by traverse
By using array + rowid:
var rowid = $("td[headers='SAL'] input:visible:eq(2)").attr("id").substr(3);
console.log(rowid);
console.log($("#f04"+rowid+""));
console.log($("#f03"+rowid+""));
console.log($("#f06"+rowid+""));
select multiple by substr

Practical example

In practice this will mostly be used on items that will have to change something in another column when they themselves have been changed. Thus, usually in change events.
Example, if DEPTNO changes, then change SAL
$("td[headers='DEPTNO'] select").change(function(){
   //get the input item in column sal
   var lSal = $(this).closest("tr").find("td[headers='SAL'] input:visible");
   //change the salary depending on department
   switch($(this).val()){
      case 10:
      lSal.val(1000);
      break;
      case 20:
      lSal.val(2000);
      break;
      case 30:
      lSal.val(3000);
      break;
      case 40:
      lSal.val(4000);
      break;
   };
});
This is easily translated to a dynamic action too. With change as event, and using "td[headers='DEPTNO'] select" as jQuery selector (without enclosing double quotes, mind you). A true action of execute javascript, and code:
var lSal = $(this.triggeringElement).closest("tr").find("td[headers='SAL'] input:visible");
switch($(this.triggeringElement).val()){
   case 10:
   lSal.val(1000);
   break;
   case 20:
   lSal.val(2000);
   break;
   case 30:
   lSal.val(3000);
   break;
   case 40:
   lSal.val(4000);
   break;
};
Now if you have to make a change to one of the columns or add one in, you'll be a lot safer. Of course, things will still break when you do certain things: removing a column, changing the type of a column (fe text to display only, text to select list), changing column headers.

Still I think this wins out. Code is a lot clearer to read when you can refer to column headers than having to find out associated array names.

Thursday, February 14, 2013

Tabular form: select elements with jQuery

One of the most frequent issues i see on the Apex forums is having to deal with jQuery selectors. Honestly, I don't believe this is hard at all.
By far the most important thing to grasp is understanding HTML. What element represents what i see on my screen? This is simply finding out what HTML apex has generated for your page.

First of all though, this is not a total tutorial on how to get anything you want. Go and read this jQuery tutorial to start you off!
http://docs.jquery.com/Tutorials:How_to_Get_Anything_You_Want_2
To better understand selectors and what is possible or what i use, refer to the api documentation which will do a far better job of explaining and demonstrating than i would.
http://api.jquery.com/category/selectors/

I'll be using Firefox and Firebug, and that's what you'll see in the couple of screenshots. Look, if you are serious about developing for the web and you'd like to understand what you're doing, then having a browser with decent tools is a requirement. For me, that is Firefox with the Firebug extension. I'm sure Chrome can work great for you too, but here I'm using Firefox. As for IE: I'll only ever open up the debug tools there when I really can't fix or reproduce in another browser.
Every Apex developer who wants to step into some javascript or jQuery development or debug his dynamic actions, investigate mark-up, investigate applied CSS, inspect AJAX calls,... needs to get this stuff sorted. Don't put it besides, beneath or above you.
You can find more information about Firebug here: http://getfirebug.com/whatisfirebug

Now to take a common problem as an example: targetting items in a tabular form. I created a tabular form on EMP with this SQL:
select 
"EMPNO",
"EMPNO" EMPNO_DISPLAY,
"ENAME",
"HIREDATE",
"SAL",
"DEPTNO",
NULL CHECKME
from "#OWNER#"."EMP"

tabform_overview
  • EMPNO: hidden
  • ENAME: popup lov, with query based lov:
    SELECT ename d, ename r FROM emp ORDER BY ename;
  • HIREDATE: date field
  • DEPTNO: select list based on a query:
    SELECT dname d, deptno r FROM dept ORDER BY dname;
  • CHECKME: simple checkbox, with values "Y,"
(This form isn't meant to do anything but serve a demonstration purpose.) What i want to do is to construct a jQuery selector which will match the text items in the "Salary" column. To be able to do that, I need to know what the markup looks like, and where in the markup these items are situated. Right-clicking the first SAL-item in the form and selecting "Inspect element with Firebug" will open up Firebug (if it wasn't already) and show the HTML tab. The selected (right-clicked) element will be highlighted in the HTML.
tabform_sal_select
This is a piece of the HTML here, and the input element will be highlighted:
<tr class="highlight-row">
   <td class="data" headers="CHECK$01">
   <td class="data" headers="EMPNO_DISPLAY">7839</td>
   <td class="data" headers="ENAME">
   <td class="data" headers="HIREDATE">
   <td class="data" headers="SAL">
      <label class="hideMeButHearMe" for="f05_0001">Sal</label>
      <input id="f05_0001" type="text" value="5001" maxlength="2000" size="16" name="f05" autocomplete="off">
   </td>
   <td class="data" headers="DEPTNO">
   <td class="data" headers="CHECKME">
</tr>
When you hover over an element in this HTML section, the element will be highlighted on the actual page too!
tabform_sal_select_firebug
So that is one element. an INPUT in a TD element in a TR element. A TD element is a table CELL, a TR element is a table ROW. To take this a bit further, i expanded a second TR element (thus: another row).
<table class="report-standard" cellspacing="0" cellpadding="0" border="0" summary="">
   <tbody>
      <tr>
      <tr class="highlight-row">
      <tr class="highlight-row">
      <tr class="highlight-row">
         <td class="data" headers="CHECK$01">
         <td class="data" headers="EMPNO_DISPLAY">7782</td>
         <td class="data" headers="ENAME">
         <td class="data" headers="HIREDATE">
         <td class="data" headers="SAL">
            <label class="hideMeButHearMe" for="f05_0003">Sal</label>
            <input id="f05_0003" type="text" value="2450" maxlength="2000" size="16" name="f05" autocomplete="off">
         </td>
         <td class="data" headers="DEPTNO">
         <td class="data" headers="CHECKME">
      </tr>
      <tr class="highlight-row">
         <td class="data" headers="CHECK$01">
         <td class="data" headers="EMPNO_DISPLAY">7566</td>
         <td class="data" headers="ENAME">
         <td class="data" headers="HIREDATE">
         <td class="data" headers="SAL">
            <label class="hideMeButHearMe" for="f05_0004">Sal</label>
            <input id="f05_0004" type="text" value="2975" maxlength="2000" size="16" name="f05" autocomplete="off">
         </td>
         <td class="data" headers="DEPTNO">
         <td class="data" headers="CHECKME">
      </tr>
      <tr class="highlight-row">
How about constructing a selector then. Well, tabular forms will generate their markup wtih the column header specified in the headers attribute of each data cell. Since there is no real way to select a certain type of column (there are row elements, but no column elements evidently) we can gather all the cells which have a matching headers attribute to the header I'm looking for.
To check a selector (or run any piece of javascript code) you have to switch to the "Console" tab in Firebug. The first time you go there, you will have the command editor in single line mode. I prefer to have it in multi-line myself, especially when running a block of code.
To target the cells I want I can use this selector:
td[headers='SAL']
You don't just run this selector of course. The selector is used in jQuery, so wrap it accordingly:
$("td[headers='SAL']")
This will tell jQuery to fetch all elements who match this selector. It does not have to be a single element, as multiple matching elements will result in an array. After hitting run you will get the output in the console output. You can tell something is an array when it is between brackets ("[...]"). Now when you hover over each element in the array each will also be highlighted on the page; or when hovering over the array without targetting a specific element, all elements in the array will be highlighted on the page.
tabform_select_array_firebug
And again, I want to stress how important it is to know your HTML. The SAL field was a simple text-type input item. A checkbox is also an input item, but is type=checkbox. And a select list is a SELECT element. This is important for your selectors of course. In the next code excerpt you can see the items for column DEPTNO and CHECKME, respectively a select list and a checkbox input.
<td class="data" headers="DEPTNO">
   <label class="hideMeButHearMe" for="f06_0001">Deptno</label>
   <select id="f06_0001" name="f06" autocomplete="off">
</td>
<td class="data" headers="CHECKME">
   <label class="hideMeButHearMe" for="f07_0001">Checkme</label>
   <input id="f07_0001_01" type="checkbox" onclick="if (this.checked) {apex.jQuery('#f07_0001').val('Y');} else {apex.jQuery('#f07_0001').val('');}" value="Y" name="f07_NOSUBMIT" autocomplete="off">
A last thing to mention is that apex will append all hidden items to the last visible input element (or select list). If you hide columns in a tabular form, the elements will be rendered to the page, albeit as a hidden input element. For example, take my column EMPNO which is hidden. This is the item in array f02 below. But the item is appended in column CHECKME, it does NOT have an own column!
<td class="data" headers="CHECKME">
   <label class="hideMeButHearMe" for="f07_0001">Checkme</label>
   <input id="f07_0001_01" type="checkbox" onclick="if (this.checked) {apex.jQuery('#f07_0001').val('Y');} else {apex.jQuery('#f07_0001').val('');}" value="Y" name="f07_NOSUBMIT" autocomplete="off">
   <input id="f07_0001" type="hidden" value="" name="f07" autocomplete="off">
   <input id="f02_0001" type="hidden" value="7839" name="f02" autocomplete="off">
   <input id="fcs_0001" type="hidden" value="8FA19154DC35EFF7C74623AE376FD8E9" name="fcs" autocomplete="off">
   <input id="frowid_0001" type="hidden" value="AAAWFmAAjAAAGA7AAA" name="frowid" autocomplete="off">
   <input id="fcud_0001" type="hidden" value="U" name="fcud" autocomplete="off">
</td>
Once you're familiar with the HTML markup that is generated, you won't go back to check each and every time of course. But keep in mind that different themes and templates can affect your selectors, as in: not working anymore. That is a good time to inspect your HTML once again and figure out what is wrong, not in the least by running your selector from the console.

Tuesday, February 12, 2013

Checkbox in tabular form column header

Some javascript code to add a checkbox in the header(s) of a tabular form, which will check/uncheck all checkboxes in a column.
/**
  * Adds a checkbox in the column header of the column identified by pColumnHeader
  * Checking and unchecking this checkbox will check or uncheck all visible 
  * checkboxes in the column
 **/
function addHeaderCheckbox(pColumnHeader){
   $("<input type='checkbox' id='"+pColumnHeader+"_chk_all_rows' />")
   .bind("click", function(){
      $("td[headers='"+pColumnHeader+"'] input[type='checkbox']:visible")
      .prop("checked", $(this).prop('checked'));
   })
   .prependTo("th#"+pColumnHeader+"");
};
Example situation:
Tabular form based on SQL:
select 
"EMPNO",
"EMPNO" EMPNO_DISPLAY,
"ENAME",
"HIREDATE",
"SAL",
"DEPTNO",
NULL checkme
from "#OWNER#"."EMP"
Where the CHECKME column is displayed as a "Simple Checkbox", values "Y,"
Since the headers are also replaced when the region is refreshed or paginated, the checkbox will have to be added to the form after each refresh. Dynamic action, "After refresh", on tabular form region.
As true action select "Execute Javascript Code", and make sure to check "Fire on Page Load".
addHeaderCheckbox("CHECKME");
As discussed in OTN thead all Check box check / uncheck in tabular form.

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.

Wednesday, November 28, 2012

Using the Modal Page plugin as a modal popup list in a tabular form

Props

I use a plugin by SkillBuilders, the Modal Page, which is free by the way. Pay them a visit here: Skillbuilders.com

Example application

Can be found over at http://apex.oracle.com/pls/apex/f?p=69293
You can download the application aswell. It comes with supporting objects (2 tables and some seeding data) and is pretty much selfcontained. The application is developed in 4.1 and imports into 4.2 without problems.

Diving straight in

Page 1

Tabular form: wizard generated and based on table DEMO_BOOK_STORE. I added in a column to be used as a link column (popuplist):
select 
"ROWID",
"BOOK_ID",
"SUBJECT",
"PRICE",
"AUTHOR",
"TOPIC",
NULL popuplist
from "#OWNER#"."DEMO_BOOK_STORE"
I hid the book_id and topic columns. I specified link column properties for column popuplist
  • Link Text: Select Item
  • Link Attributes: onclick="return false;" class="modalshow"
  • Target: Page in this Application
  • Page: 2
Add Button: I changed the button action from "Redirect to URL" to "Defined by Dynamic Action". This is because an extra action needs to happen when a row is added. In page attributes: Javascript:
var gaInputMapping = new Array();
var gRowSelect, gTriggerTable;
$().ready(function(){
   $("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};
      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 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 getObject(pHeader){
   var lRet;
   $.each(gaInputMapping, function(index){
      if(gaInputMapping[index].header==pHeader.toUpperCase()){
         lRet= gaInputMapping[index];
      };
   });
   return lRet;
};
function setRowDetails(pTable, pRow, pItemId){
   $.post('wwv_flow.show', 
          {"p_request"      : "APPLICATION_PROCESS=get_item_details",
           "p_flow_id"      : $v('pFlowId'),
           "p_flow_step_id" : $v('pFlowStepId'),
           "p_instance"     : $v('pInstance'),
           "x01"            : pItemId},
           function(data){              
              var lItem = $.parseJSON(data);
              $.each(lItem, function(){
                 var lSelector = getSelector(this.COLUMN);
                 if(lSelector){
                    $(lSelector, pTable.find('tbody > tr').eq(pRow)).val(this.VALUE).change();
                 }
              })
           });
};
HTML Header and Body Attribute: HTML Header:
<script type="text/javascript" src="#IMAGE_PREFIX#libraries/jquery-ui/1.8.14/ui/minified/jquery.ui.button.min.js"></script>
4 Dynamic actions:
"Button Icons"
  • Event: After Refresh
  • Selection Type: Region
  • Region: tabular form region
  • Event Scope: bind
True action: Execute javascript code
  • Fire on page load: checked
  • Code:
$(this.triggeringElement).find("a.modalshow").button({icons:{secondary:'ui-icon-circle-arrow-n'}});
"Modal closed: fetch details"
  • Event: Manual Close [SkillBuilders Modal Page]
  • Selection Type: DOM Object
  • DOM Object: document
  • Event Scope: bind
True action: Execute javascript code
  • Fire on page load: unchecked
  • Code:
if(this.data.modalPageId=='selectitem'){
   lItemId = this.data.modalPageCloseValue.item_id;
   if(lItemId){
      setRowDetails(gTriggerTable, gRowSelect, lItemId);
   };
};
"Show modal"
  • Event: Click
  • Selection Type: jQuery Selector
  • jQuery Selector: .modalshow
  • Event Scope: live
True action: Execute javascript code
  • Fire on page load: unchecked
  • Code:
gTriggerTable = $(this.triggeringElement).closest('table');
gRowSelect = $(this.triggeringElement).closest('table').find('tbody > tr').index($(this.triggeringElement).closest('tr'));
True action: SkillBuilders Modal Page [Plug-in]
  • Fire on page load: unchecked
  • Dialog Title: Select an item
  • URL Location: Statically Defined
  • Static URL: f?p=&APP_ID.:2:&APP_SESSION.:::2:::
  • Dialog Height/Width Mode: Static by pixels
  • Height: 700
  • Width: 600
  • Modal Page ID: selectitem
"Add Row"
  • Event: Click
  • Selection Type: Button
  • Button: ADD (Add Row)
  • Event Scope: bind
True action: Execute javascript code
  • Fire on page load: unchecked
  • Code:
addRow();
$(this.affectedElements[0]).find("a.modalshow:last").button({icons:{secondary:'ui-icon-circle-arrow-n'}});
  • Affected Elements:
    • Selection Type: Region
    • Region: tabular form region
Page Process:
  • Type: PL/SQL anonymous block
  • Process Point: On Demand
  • Code:
declare
   lJSON VARCHAR2(1000);
begin
   select '[{"COLUMN":"BOOK_ID", "VALUE":"' || book_id ||'"},'||
           '{"COLUMN":"SUBJECT", "VALUE":"' || subject ||'"},'||
           '{"COLUMN":"PRICE",   "VALUE":"' || price   ||'"},'||
           '{"COLUMN":"AUTHOR",  "VALUE":"' || author  ||'"},'||
           '{"COLUMN":"TOPIC",   "VALUE":"' || topic   ||'"}]'
     into lJSON
     from demo_book_list
    where book_id = apex_application.g_x01;

   htp.p(lJSON);
end;

Page 2

Page template: I made a copy of the popup page template in the theme. In my example I am using theme 23 and since the popup template has a table layout with a useless right column, I removed the table markup and just replaced it with a div. This is my “Body” code:
#SUCCESS_MESSAGE##NOTIFICATION_MESSAGE##GLOBAL_NOTIFICATION#
<div>
#REGION_POSITION_01#
#REGION_POSITION_02#
#BOX_BODY#
#REGION_POSITION_04#
#REGION_POSITION_05#
#REGION_POSITION_06#
#REGION_POSITION_07#
</div>
Standard report based on DEMO_BOOK_LIST, with 3 columns: book_id, subject, author. Edit column BOOK_ID and change heading to 'Select'. Specify Column Link attributes:
  • Link Text: Select me!
  • Link Attributes: onclick="return false;" class="modalclose"
  • Target: URL
  • URL: #BOOK_ID#
1 Dynamic Action:
"Button Icons"
  • Event: After Refresh
  • Selection Type: Region
  • Region: report region
  • Event Scope: bind
True action: Execute javascript code
  • Fire on page load: checked
  • Code:
$(this.triggeringElement).find("a.modalclose")
.button({icons:{secondary:'ui-icon-circle-arrow-e'}})
.click(function(){
   var retval = {"item_id":$(this).attr('href')};
   parent.$(parent.document).apex_modal_page('close', retval);
});

Some Clarifications

javascript in page 1:
Input mappings:
var gaInputMapping = new Array();
var gRowSelect, gTriggerTable;
$().ready(function(){
   $("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};
      gaInputMapping.push(lMap);
   });
});
This builds of the input map that apex generates for wizard generated tabular forms. This will read the map and store it in a global javascript array variable. The type of the referenced item will also be stored, meaning that whether the item is a select list or an input item.
The columns in the map are also generated in the same order as the assigned f##-arrays for each column. So the first item in the fmap array will be array f01, the second will be f02,...
<input id="fmap_001" type="hidden" value="CHECK$01" name="fmap">
<input id="fhdr_001" type="hidden" value="Select Row" name="fhdr">
<input id="fmap_002" type="hidden" value="BOOK_ID" name="fmap">
<input id="fhdr_002" type="hidden" value="Book Id" name="fhdr">
<input id="fmap_003" type="hidden" value="TOPIC" name="fmap">
<input id="fhdr_003" type="hidden" value="Topic" name="fhdr">
<input id="fmap_004" type="hidden" value="PRICE" name="fmap">
<input id="fhdr_004" type="hidden" value="Price" name="fhdr">
<input id="fmap_005" type="hidden" value="AUTHOR" name="fmap">
<input id="fhdr_005" type="hidden" value="Author" name="fhdr">
<input id="fmap_006" type="hidden" value="SUBJECT" name="fmap">
<input id="fhdr_006" type="hidden" value="Subject" name="fhdr">
The several functions below that are functions allowing easy retrieval of a column or item through either the headers or the array name.
Understand that this is a different approach from retrieving input items through the headers on a TD! In particular, hidden items can ruin your plans of referencing items. In my sample app i have 3 hidden columns: rowid, book_id and topic. Apex will generate these hidden items in the last column:
<td headers="SUBJECT">
<label class="hideMeButHearMe" for="f06_0001">Subject</label>
<input id="f06_0001" type="text" value="Easy Oracle" maxlength="2000" size="40" name="f06" autocomplete="off">
<input id="f02_0001" type="hidden" value="2" name="f02" autocomplete="off">
<input id="f03_0001" type="hidden" value="5" name="f03" autocomplete="off">
<input id="fcs_0001" type="hidden" value="2C9BDC219934F8ED33AF9602262E2391" name="fcs" autocomplete="off">
<input id="frowid_0001" type="hidden" value="AAAqsGAAjAAAWQrAAA" name="frowid" autocomplete="off">
<input id="fcud_0001" type="hidden" value="U" name="fcud" autocomplete="off">
</td>
So when you try to reference the input items through columns headers: $("td[headers='SUBJECT'] input") you'll be greeted by an array of 6 inputs. Of course, you could then add the :visible filter to only target the visible text item. But how will you deal with the hidden items then? Assign something to them? You will need to hardcode the arrays somewhere.
By reading the fmap array this can be avoided. And items which are hidden are still targetable through the headers, which is a lot more intuitive than using the f## arrays (which change when you change the column ordering or add or remove a column...). Retrieve details:
function setRowDetails(pTable, pRow, pItemId){
   $.post('wwv_flow.show', 
          {"p_request"      : "APPLICATION_PROCESS=get_item_details",
           "p_flow_id"      : $v('pFlowId'),
           "p_flow_step_id" : $v('pFlowStepId'),
           "p_instance"     : $v('pInstance'),
           "x01"            : pItemId},
           function(data){              
              var lItem = $.parseJSON(data);
              $.each(lItem, function(){
                 var lSelector = getSelector(this.COLUMN);
                 if(lSelector){
                    $(lSelector, pTable.find('tbody > tr').eq(pRow)).val(this.VALUE).change();
                 }
              })
           });
};
This will call the application process and process the data. A book id is passed in and it will receive the details. The process will return a json string:
   select '[{"COLUMN":"BOOK_ID", "VALUE":"' || book_id ||'"},'||
           '{"COLUMN":"SUBJECT", "VALUE":"' || subject ||'"},'||
           '{"COLUMN":"PRICE",   "VALUE":"' || price   ||'"},'||
           '{"COLUMN":"AUTHOR",  "VALUE":"' || author  ||'"},'||
           '{"COLUMN":"TOPIC",   "VALUE":"' || topic   ||'"}]'
     into lJSON
     from demo_book_list
    where book_id = apex_application.g_x01;
This will parse to an array of objects. Each object holds the column name and the value. I did this simply because it is easier to loop over and generalize. The javascript will loop over the returned array, and then for each column it'll find the matching input mapping and return a selector. With this selector the correct input in the same row can be manipulated. Take note of the jQuery selector:
$(lSelector, pTable.find('tbody > tr').eq(pRow)
This will find the input item within the given context: the clicked row in the clicked table. pTable and pRow are set when the 'select item' button is clicked in global variables. Dynamic action "Show Modal"
gTriggerTable = $(this.triggeringElement).closest('table');
gRowSelect = $(this.triggeringElement).closest('table').find('tbody > tr').index($(this.triggeringElement).closest('tr'));
Again, this allows for avoiding the tedious item_id parsing which includes the 'rownum'. And i prefer this way over targeting the item array and picking out one at the row index. Again, take not of the triggering of the change event on each column. When no change event is triggered, a new row will not be marked as a changed row but rather remain marked as deleted, and no insert will occur (same logic for updating a row of course).
Basically, in sequence:
  1. click the button
  2. table and row are saved in js
  3. modal page is opened
  4. item is selected
  5. modal page is closed and a return value is specified (book id)
  6. the book details are retrieved through ajax
  7. in the ajax success callback the book details are filled out into the items of the (saved) clicked row

So

It might look a little daunting, but it doesn't have to take too long. Some of the javascript can easily be put in a file and then included. The jquery buttons are also a bit extra-curricular, but i like them for being easy to implement and how they improve look and feel.
The modal page requires some setup, and you will need to create a page to be used in the modal popup. However, i consider this a strength as it allows for maximum flexibility. I'm providing this code and example as an example of how you could use the modal popup and javascript to greatly enhance a tool. This could be a tabular form, an item on a page, a button in an interactive report,...
I also implemented the ajax retrieval of the book details as part of the example. You could skip ajax and return all details from the modal page through its return value object! But this is cooler ;-)
And last but not least, you can just create and maintain a standard wizard generated tabular form. No need to grasp for apex_item generated forms this way, which is a real strength in my opinion.