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.

No comments:

Post a Comment