Navigating through an ADF editable table with arrow keys

7

For a client that uses a lot of ADF editable table I was exploring the possibilities of navigating through an editable table with the arrow keys. They often need to edit values in one column for all the rows in the table. The only way to do that now is using the mouse or keep clicking the tab key until the focus ends up in the field you want. That’s not really good user experience, so we implemented a generic arrow navigation script that works on all editable tables in your application.

Navigating (read record selection) in a read only table is out-of-the-box supported by the framework. In an editable table this also works when the focus is not on an input component but on the table cell. When the focus is in an input field and I click on the up/down arrow keys… nothing happens! (well actually the cursor is placed in front (key up) or behind (key down) the value of your input component but that is not what we want)

I will use the HR.EMPLOYEES table as an example.

ADF editable table navigation

Arrow key navigation in an ADF editable table is not available by default

Let’s say we want to alter the salary for all employees. There are 2 ways to do that:
– Alter salary in row 1, select row 2 with your mouse, alter value in row 2, select row 3 with your mouse…
– Alter salary in row 1, click tab 7 times, alter value in row 2, click tab 7 times…

Both are not really an option if you need to edit this regularly so we came up with the following solution.

Important note: This solution contains calls to ADF internal javascripts objects. These are intended to be used only internally by the ADF Faces component framework. Changes to internal objects are applied without further notice and with no guarantee for backward compatibility, which is why application developers should only use the public api’s.
That having said… Sometimes necessity knows no law.

First we register a javascript keyHandler in the onload of the main page

    <af:document title="EditableTable.jsf" id="d1">
        <af:resource type="javascript" source="/resources/js/EditableTableNavigation.js"/>
        <af:clientListener method="registerKeyBoardHandler" type="load"/>

This will run the registerKeyBoardHandler function in the EditableTableNavigation.js

function registerKeyBoardHandler(event) {
    for (var i = keyRegistry.length - 1; i >= 0; i--)
    {
      var keyStroke = AdfKeyStroke.getKeyStrokeFromMarshalledString(keyRegistry[i]);
      AdfRichUIPeer.registerKeyStroke(event.getSource(), keyStroke, callBack);
    }
}

// defines the keys that are registered to be handled by the callback function in this script
var keyRegistry = new Array();
keyRegistry.push("UP");
keyRegistry.push("DOWN");

Now every up or down arrow keystroke will call the callBack function below. I added comments in the code sample below to guide you through the code.

function callBack(keyCode)
{
    var ac = AdfPage.PAGE.getActiveComponent();
    var marshalledKeyCode = keyCode.toMarshalledString(keyCode);
    var componentType = ac.getComponentType();
    if(marshalledKeyCode == "UP" || marshalledKeyCode == "DOWN")
    {
      // Ignore arrow actions in drop down list and mult-line text fields
      // NOTE: for getRows to return correct value, 
      // clientComponent must be set to true on inputText!
      if(componentType=='oracle.adf.RichSelectOneChoice' || ac.getProperty("rows")>1)
      {
        // don't handle as function key up/down
        return false;
      } else if (marshalledKeyCode == "UP" || marshalledKeyCode == "DOWN") {
        // Let's try and find out if we are in a table
        var parent = ac.getParent();
        while (parent)  {
          if (parent instanceof AdfRichTable) {
              break;
          }
          parent = parent.getParent();
        }
        // Did we find a table?
        if (parent instanceof AdfRichTable) {
          //using a matcher to split up the clientId 
          // so we can calculate the new clientId we want to navigate to
          var clientId = ac.getClientId().match("(.*):([0-9]+)(:[^:]+)");
          var newRowKey
          var tablePeer  = parent.getPeer();
          tablePeer.bind(parent);
          // ask for the previous/next rowkey
          switch (marshalledKeyCode) {
            case "UP":
                newRowKey = tablePeer._getPreviousRowKeyAndRow(clientId[2]).rowKey;
                break;
            case "DOWN":
                newRowKey = tablePeer._getNextRowKeyAndRow(clientId[2]).rowKey;            
                break;
          }
          // Check if page is ready (cancel navigation if not)
          // Otherwise when holding down the arrow keys the navigation gets out of sync
          if (!AdfPage.PAGE.isSynchronizedWithServer()) {
              return true;
          }
          // Handle the table row selection (but not the focus to the input field)
          tablePeer._handleTableBodyArrowUpDown(marshalledKeyCode == "UP", false, false);
          // For a click to edit table we need a more extensive approach
          if(!tablePeer._isClickToEdit()) {
            // Edit all mode
            // We use the beginning and the end of our old clientId and replace the rowKey part
            var newComp = AdfPage.PAGE.findComponent(clientId[1] + ":" +newRowKey + clientId[3]);
            // If we can find the component, set focus
            if (newComp!=null) {
              newCompPeer = newComp.getPeer();
              newCompPeer.bind(newComp);
              // find the html input element, set focus and select the value 
              // (so you can edit it immediately)
              var inputElem = newCompPeer.getDomContentElement().firstElementChild;
              inputElem.focus();
              inputElem.select();
              var counter = 0;
             // Check every 10ms if the page is ready (in case of PPR focus gets lost)
              var interval = setInterval(function () {
                if (++counter === 100 || AdfPage.PAGE.isSynchronizedWithServer()) {
                  inputElem.focus();
                  inputElem.select();
                  clearInterval(interval);
                }
              },10);
            }
          } 
          return true;
        }
        // We are not in a table, handle as usual.
        return false;
    }
  } 
}

With this code in place you can navigate vertically through the editable table with the arrow key’s. There is a limitation on date fields because they have a keyhandler on the down arrow that opens the date popup. The only way to fix that is by overriding an ADF internal method.
Below you will find a link to a ADF 12.2.1 project containing a working sample. This sample also contains code for handling the arrow keys in a click-to-edit table.

Download the ADF editable table navigation 12.2.1 demo workspace

About Author

Paul Swiggers is Principal Consultant and Expertise Lead ADF at AMIS, specialized in Oracle's Application Development Framework (ADF) and (PL/)SQL, playing around with Oracle SOA/OSB.Experienced trouble shooter, lead developer and coach, loves to explore tools that help building better applications like the Oracle Application Testing Suite (OATS) for performance testing and Oracle Real User Experience Insight (RUEI)Big fan of Agile development in multidisciplinary teams. Exploring how tools like Selenium, Bamboo, Sonar can help delivering more reliable software.

7 Comments

  1. what if i reached fetch size, table will fetch data and i will lose focus on input text
    it only select next row and don’t setFocus to input text

  2. Hi Paul
    Thank you for your solution, moreover could you help me with LEFT and RIGHT arrow for moving between 2 column inside the table.
    Best regards

  3. Amazing solution! I have a master detail table and to make more fluid navigation is good to set SelectionEventDelay to enabled.

    Thank you very much!

  4. OK very cool and all that but also problematic I feel. There are 3 fundamental issues with this approach:

    1. As you say it uses the peer classes to do stuff which we don’t allow. It’s all very well sprinkling disclaimers through the article here but that won’t help in 12 months time when some-ones code breaks during an upgrade and they try and log it as a support issue. Sadly folks google, copy and paste and ignore the warnings.

    2. The technique ignores all of the other types of components that might be involved in the table (e.g. inputNumberSpinbox and many many more) that also have an interest in the arrow keys. Sure you could add the ones you need for just this table to the code, but again when the table is updated in a years time with an extra component will the person who has copied this remember to make that change?

    3. The user experience becomes inconsistent if sometimes the arrow keys navigate the grid and at other times they do not. This frankly, is why we don’t navigate with the arrow keys today in an editable table. It’s an accessibility nightmare.

    When faced with a requirement like this I’d always push back and investigate other ways of achieving the goal. If what the use case requires is really a spreadsheet then look at using ADFdi and give them a real spreadsheet. If the use case requires bulk update of multiple rows at once with the same value (or a series of values) then look at other ways of expressing that.

    Having delt with the fallout of this kind of hacking in the past I thought that I’d better put my oar in…

    Duncan

    • Hi Duncan, thanks for your reply. All very valid points.
      We are very well aware of the downsides of this solution, we tried alternatives but at the end this solution was an exact match to our clients needs.
      I do think I underestimated the “folks google, copy and paste and ignore the warnings” part.
      Let’s hope that after reading this comment they do copy the warnings as well…
      Regards,
      Paul

  5. Does it work on ClickToEdit Tables? and does SelectionListener Event fire when going from row to row?

    • Hi Gary,
      I left the clickToEdit implementation out of the inline code sample because it is a bit more complex than the editAll implementation.
      At the bottom of the post you can find a download link of a test project with a working sample (ADF 12.2.1) for editAll and clickToEdit tables. Have a look at the js file in the zip to see the actual clickToEdit implementation.
      The selectionListener events will fire when navigating.
      Regards,
      Paul