UIX Developer's Guide
Go to Table of Contents
Contents
Go to previous page
Previous
Go to next page
Next

10. Tables

The table is perhaps both the most powerful and most complicated user interface component available in UIX Components. Any web application that has to display or update tabular data will likely need to use at least one table. This chapter examines all the different ways to configure a table component.

This chapter contains the following sections:

Should I Use a Table?

Many developers presented with the array of options available in the Table assume that it is meant to solve any problem; this is not the case! The Table is provided specifically to present (and possibly update) data which is naturally described in a two dimensional format. Here are some rules of thumb that indicate a situation where using the Table makes sense:

If these rules do not apply, a Table is probably not the appropriate widget to use. Some of the following heuristics imply appropriate use of tables, although not all apply in every situation:

On the other hand, there are some motives which do NOT justify the use of a Table:

If your design still warrants the use of a Table, it's time to start learning how to create one!

Table Concepts

When designing your Table's appearance, the core concept to keep in mind is that data items being compared exist as rows, and the facets or actions to take on those items exist as columns. Since we've established that the items should be similar in nature, the number of facets to them should also be consistent. Thus, the columns in a Table should be the same for every row. However, the number of data items to display, and consequently the number of rows, is likely to vary. For instance, a Table used to represent a shopping cart in a web application might have no items at the outset, but multiple items in it after a round of shopping.

Each of the intersections of a data item row and a column is referred to as a cell. Thus, the table becomes a two dimensional display of many cells, with one row of cells for each data item and with any given column of cells representing the same facet or action compared across many data items.

Tables will also need labels to keep the data straight in the user's mind. For these, we use column and row headers: column headers across the top label which facet or action that column contains -- like the price of a shopping cart item; row headers down the left can be used before each data item to impose a visible ordering strategy. Sometimes it is also useful to present a summary of the data, and in these cases we use a column footer at the bottom of the table. For the sake of consistency, we also refer to the column header and footer items as having cells.

Other formatting options can be used in a Table for advanced displays and additional information, but we will introduce them throughout this chapter. For now, let's create some tables.

Stamps

Before we go any further, let's see our first UIX example. Not surprisingly, this is the most basic table.

  <table xmlns="http://xmlns.oracle.com/uix/ui"/>

That example is dull by design, and in fact it won't display a thing. In it, we've only declared that we want to show a table element in the UIX Components namespace. We haven't actually configured the table yet, which is why it looks blank. In order to display something, we need to add some data items and columns, which is done through "stamps".

What is a stamp? A stamp is just a UINode -- any UINode -- that is rendered more than once on a given page. It is called a "stamp" because, like the stamp someone uses to mark a paper document "Confidential", our stamp always produces the same mark whenever it is used. A stamp can be a button, a text field, or any other widget that needs to be repeated. In the Table, there is one stamp per column, and this stamp is repeated, or rendered, down all the cells of that table column. To add a column stamp to a table, simply give the table an indexed child. Each indexed child becomes a new column stamp, with the first indexed child becoming the left-most column stamp, and subsequent children being added to the right of the last stamp.

Why do we use the stamp concept to render data in a table column? Recall that tables should be used to compare similar data items. Since each column represents a facet of a data item, it makes sense that the appearance of the cells in a given column would be similar. Making stamps similar in each cell -- but not exactly the same -- is explained later in this chapter.

Let's add our first column stamp to our table example:

<table ... >
  <contents>
    <!-- the first column stamp, a text node -->
      <text text="SampleText"/>
  </contents>
</table>

Unfortunately, even though we now have a stamp in our table there still isn't anything interesting displayed. In order to explain why, we need to introduce the cardinal rule of the table:

Every stamp gets rendered once for every DataObject

According to this rule, if we don't have any DataObjects the stamps won't get rendered, so let's add some data.

The DataObjects we need are supplied by the first attribute we will examine, tableData, which represents the rows in the table. As previously mentioned, the number of rows in a table can vary across its life (for instance, as the user adds or removes items from a shopping cart). As is the case with all UIX Components concepts that involve variable length lists, this tableData attribute will have a DataObjectList as its type. Recall that a DataObjectList does only two things: it reports its length and it allows access to each DataObject in it by index. For the tableData, the length of the DataObjectList determines the number of rows displayed, and each DataObject in the list provides data for its corresponding row.

Let's put some sample data in our existing example. Since this is just a demo, we will use uiXML's inline data capability. Most real applications would expose their data via more complex, dynamic sources, however.

<dataScope xmlns="http://xmlns.oracle.com/uix/ui"
           xmlns:data="http://xmlns.oracle.com/uix/ui">
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        <!-- all the row DataObjects used by our table for tableData -->
        <demoRowData/>
        <demoRowData/>
        <demoRowData/>
      </inline>
    </data>
  </provider>

  <contents>
    <table data:tableData="demoRowData@demoTableData">
      <contents>
        <!-- the first column stamp, a text node -->
          <text text="SampleText"/>
      </contents>
    </table>
  </contents>
</dataScope>

We've introduced a few things into our demo code now:

  1. We've put our table into a <dataScope> element, which will provide the table with a data source. The table itself is moved into the contents of the <dataScope>, and the data is placed in the <provider> section.
  2. Our data is specified using uiXML's inline data syntax and stored under the name "demoTableData" for this example. Inside the "demoTableData" we've declared three "demoRowData" elements. Because these three data elements all have the same element name, uiXML will merge them into one DataObjectList with a size of three and with each element in turn representing one of the three index-based DataObjects in the DataObjectList.
  3. The table element has a new attribute -- data:tableData -- which supplies the table with the DataObjectList it needs to represent its rows. Specifically, the "demoRowData" DataObjectList we just placed in the provider section is selected to be the row data source.

After running this example, you will see that the table finally displays three cells in a vertical column, each containing some identical sample text. This is because we have a single column stamp -- the text element -- and three DataObjects in our tableData list; according to the table rule, our text stamp will be rendered once for every DataObject in the list. Thus, we see the text three times.

Let's see what happens when we change things a bit. First, we will add a fourth data object to our row list. This will cause four rows to be rendered in our table, again due to the cardinal table rule indicating that each stamp is rendered once for every DataObject.

<dataScope ... >
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        <!-- all the row DataObjects used by our table for tableData -->
        <demoRowData/>
        <demoRowData/>
        <demoRowData/>
        <demoRowData/>
      </inline>
    </data>
  </provider>

  <contents>
   ...
  </contents>
</dataScope>

Next, we'll alter the table in the other dimension by adding a second column stamp, this time a button. According to the table rule, both the existing text stamp AND the new button will each be rendered four times.

<dataScope ... >
  <provider>
   ...
  </provider>

  <contents>
    <table data:tableData="demoRowData@demoTableData">
      <contents>
        <!-- the first column stamp, a text node -->
        <text text="SampleText"/>
        <!-- the second column stamp, a button -->
        <button text="Push Me" destination="http://www.example.org"/>
      </contents>
    </table>
  </contents>
</dataScope>

These two concepts can be extended to put any number of rows in a table, and to vary the type and number of columns. Try it and see!

Databinding in Tables

This is all well and good for tables that show the same data in every row, but practically speaking that isn't likely to occur in any real tables. In order to create useful examples, we will need databinding.

Recall that databinding is used throughout UIX Components to change the contents of a page on a per-render basis. Here we take databinding one step further by using it to change table contents on a per-row basis. Since the UINode stamped down a column doesn't change for each row, we use databinding to vary what actually gets stamped in any given cell. This makes the stamps serve a more useful purpose; the type of content they stamp in each cell of a column is similar -- for example, each cell in the column will be a text field -- but the content itself is different -- for example, the text inside each text field will be row by row.

This works by using UIX Components's concept of a "current" DataObject. As with any UIX Components databinding, attributes of a node can be bound to the current DataObject instead of a specifically named DataObject. This causes the generated result to change based on whatever DataObject happens to be current at the time of the query. The table makes this concept possible by changing the current DataObject when it renders each of the table rows. Thus, if a column stamp attribute is bound to the same key on the current DataObject, the actual value of that attribute will change in every table row!

Now the rationale for using DataObjects to determine the number of rows to render makes more sense, because each element in the table's tableData DataObjectList is used as the "current" DataObject for its corresponding row. Let's illustrate by augmenting our existing example:

<dataScope ... >
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        <!-- all the row DataObjects used by our table for tableData -->
        <demoRowData someText="First row"/>
        <demoRowData someText="Second row"/>
        <demoRowData someText="Third row"/>
        <demoRowData someText="Fourth row"/>
      </inline>
    </data>
  </provider>

  <contents>
    <table data:tableData="demoRowData@demoTableData">
      <contents>
        <!-- the first column stamp, a text node -->
        <text data:text="someText"/>
        <!-- the second column stamp, a button -->
        <button data:text="someText" destination="http://www.example.org"/>
      </contents>
    </table>
  </contents>
</dataScope>

Finally, we have a table with contents that are different in each row. Here's what we changed:

  1. We added one piece of data to each of our four "demoRowData" DataObjects, a text String which will be returned when the DataObject is queried with the key "someText". We made the result different in each row.
  2. We bound the value of the text in our first column stamp to be the result returned from querying the current DataObject with the key "someText". This query goes to the current DataObject, rather than a named DataObject, because we used the attribute value "someText" instead of "someText@aNamedDataObject". Because of this, the text we stamp on each row comes from the table data.
  3. Finally, we use the same technique to vary the text of our button stamp in the second column. Note, however, that we left the "destination" of that button the same for every row; not every attribute has to be databound.

For the sake of clarity, let's tweak the example a little so that the text and button stamps use separate data:

<dataScope ... >
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        <!-- all the row DataObjects used by our table for tableData -->
        <demoRowData firstColumnText="First row"  secondColumnText="Button #1"/>
        <demoRowData firstColumnText="Second row" secondColumnText="Button #2"/>
        <demoRowData firstColumnText="Third row"  secondColumnText="Button #3"/>
        <demoRowData firstColumnText="Fourth row" secondColumnText="Button #4"/>
      </inline>
    </data>
  </provider>

  <contents>
    <table data:tableData="demoRowData@demoTableData">
      <contents>
        <!-- the first column stamp, a text node -->
        <text data:text="firstColumnText"/>
        <!-- the second column stamp, a button -->
        <button data:text="secondColumnText" destination="http://www.example.org"/>
      </contents>
    </table>
  </contents>
</dataScope>

To do this, we gave each of our row DataObjects two text values -- "firstColumnText" and "secondColumnText" -- instead of just one. Then, we bound the text column stamp to "firstColumnText" and the button to "secondColumnText". As a result, the two columns now have different text.

Even though our examples here are simple, the implications are very powerful. Keep in mind that any UINode can be used as a column stamp, and any attribute of that stamp can be bound to the current DataObject. This provides a tremendous amount of flexibility in controlling the contents of the table cells.

You might be asking yourself a few questions as this point. Why go to all the trouble to use stamps and separate data? Why not just let developers specify the contents of each and every cell, content and data together?

There is a good reason, which is to separate the model (or data) from the view (or appearance) of the table. This allows developers to specify the appearance of the table by changing the column stamps, and later plug in any data source to supply those stamps with concrete data. If we instead specified exactly what went into each cell, we would not be able to use the same set of UIX Components nodes for every page view. We would not be able to change the number of rows in a table from render to render, or the data in each of the cells, either. Specifying the structure of the table once and reusing that structure for every page render is one of the core features of the entire UIX Components framework.

Table Data

The tableData attribute is of type DataObjectList and so can be set in a couple of ways. For static data that never changes you can inline it into the uiXML code, as in the following example:

<table ...>
 <tableData>
  <demoRowData firstColumnText="First row"  secondColumnText="Button #1"/>
  <demoRowData firstColumnText="Second row" secondColumnText="Button #2"/>
  <demoRowData firstColumnText="Third row"  secondColumnText="Button #3"/>
  <demoRowData firstColumnText="Fourth row" secondColumnText="Button #4"/>
 </tableData>

 ...
</table>

The uiXML parser knows that tableData is of type DataObjectList and will parse the children of the tableData element into a DataObjectList. The row key, in this case demoRowData, can be anything; as long as all four rows have the same row key, they will be grouped into the same DataObjectList. Incidentally, there is no data binding here; the tableData attribute is set to be the newly created DataObjectList.

One of the problems with the above approach is that the table data cannot be shared between tables. Sharing can be achieved using data binding and the following form (that has been presented before):

<dataScope ... >
  <provider>
    <data name="demoTableData">
      <inline>
        <demoRowData firstColumnText="First row"  secondColumnText="Button #1"/>
        <demoRowData firstColumnText="Second row" secondColumnText="Button #2"/>
        <demoRowData firstColumnText="Third row"  secondColumnText="Button #3"/>
        <demoRowData firstColumnText="Fourth row" secondColumnText="Button #4"/>
      </inline>
    </data>
  </provider>

  <contents>
    <table name="table1" data:tableData="demoRowData@demoTableData">
     ...
    </table>

    <table name="table2" data:tableData="demoRowData@demoTableData">
     ...
    </table>
  </contents>
</dataScope>

In the above example, the inline data provider creates a DataObject with a single key, demoRowData, that is bound to a DataObjectList. The outer DataObject is unnecessary; in the following section, we will create the table data in Java and avoid creating the outer DataObject.

Creating Table Data in Java

Data binding allows us to build DataObjectLists in Java and provide them as data for our tables. Here is a quick example:

<dataScope ... >
  <provider>
    <data name="demoTableData"> 
      <method class="test.MyTable" method="getTableData" /> 
    </data>
  </provider>

  <contents>
    <table data:tableData="@demoTableData">
     ...
    </table>
  </contents>
</dataScope>

Note the change (above) in the tableData binding. This is because we don't wrap our DataObjectList inside a DataObject. The Java code that creates the table data is presented below. (Both the uiXML and Java aspects of data binding is explained in detail in Data Binding.)

package test;

public class MyTable
{
  public static DataObject getTableData(RenderingContext context,
                                        String namespace,
                                        String name)
  {
    DataObject[] data = new DataObject[4];
    data[0] = new MyDataObject("First Row",  "Button #1");
    data[1] = new MyDataObject("Second Row", "Button #2");
    data[2] = new MyDataObject("Third Row",  "Button #3");
    data[3] = new MyDataObject("Fourth Row", "Button #4");

    // convert the array into a DataObjectList
    return new ArrayDataSet(data);
  }

  private static final class MyDataObject implements DataObject
  {
    public MyDataObject(String column1, String column2)
    {
      _col1 = column1;
      _col2 = column2;
    }

    public Object selectValue(RenderingContext context, Object key)
    {
      if ("firstColumnText".equals(key))
        return _col1;
      else if ("secondColumnText".equals(key))
        return _col2;
      return null;
    }

    private final String _col1, _col2;
  }
}

In the above example, we create our own implementation of DataObject which recognizes two keys. We then create an array of instances of our data, and make a DataObjectList out of them using the oracle.cabo.ui.data.ArrayDataSet class. Note that ArrayDataSet implements both DataObject and DataObjectList, and so can be used to return table data from within a DataProvider.

All this time we have been creating static data. Now that we are running Java code we can create dynamic data; in the following example, we create table data with a directory listing:

package test;

public class MyTable
{
  public static DataObject getDirectoryData(RenderingContext context,
                                            String namespace,
                                            String name)
  {
    // Make sure this directory exists on your file system
    return new DirDataObjectList(new File("/home/user/"));
  }

  private static final class DirDataObjectList
    implements DataObjectList, DataObject
  {
    public DirDataObjectList(File dir)
    {
      _files = dir.listFiles();
    }

    public int getLength()
    {
      return _files.length;
    }

    public DataObject getItem(int index)
    {
      // in a more prudent implementation, we would be caching these
      // DataObjects, rather than creating new ones each time.
      return new FileDataObject(_files[index]);
    }

    public Object selectValue(RenderingContext context, Object key)
    {
      // we don't support any properties on this DataObject, since this is
      // primarily a list of DataObjects.
      return null;
    }

    private final File[] _files;
  }

  private static final class FileDataObject implements DataObject
  {
    public FileDataObject(File file)
    {
      _file = file;
    }

    /**
     * This DataObject recognizes two keys: name which gives the
     * file name, and length which gives the file length.
     */ 
   public Object selectValue(RenderingContext context, Object key)
    {
      if ("name".equals(key))
        return _file.getName();
      else if ("length".equals(key))
        return new Long(_file.length());
      return null;
    }

    private final File _file;
  }
}

And the corresponding UIX code would look like:


<dataScope ... >
  <provider>
    <data name="demoTableData"> 
      <method class="test.MyTable" method="getDirectoryData" />
    </data>
  </provider>

  <contents>
    <table data:tableData="@demoTableData">
      <contents>
        <text data:text="name"/>
        <text data:text="length"/>
      </contents>
      ...
    </table>
  </contents>
</dataScope>

Another class that might help creating table data in Java is oracle.cabo.ui.data.ListDataObjectList which converts a Java Vector (and soon a JDK1.2 List) into a DataObjectList. There are other classes that help to process table data on the server side; these will be discussed in a later section.

Creating Table Headers

At this point, we have dealt only with the data portion of the table, not with the peripheral sections of the table that lie around the data. The first of these we will examine is the column header, which provides the labels for each of our columns. We will then examine the row header, which provides the labels for each row. There are actually two ways to create column headers, the first of which we will deal with now, and the second of which will be described in a later section concerning column encapsulation.

Column Headers

Like columns themselves, column headers are stamped, but in this case they are stamped across the top of the table, rather than down the cells in a column. To register a stamp node to serve as the column header, we must set it as the named child columnHeaderStamp. The following example illustrates this:

<dataScope ... >
  <provider>
   ...
  </provider>

  <contents>
    <form name="testForm">
      <contents>
        <table ... >
          <contents>
           ...
          </contents>

          <!-- add a column header stamp node -->
          <columnHeaderStamp>
            <text text="Column Header"/>
          </columnHeaderStamp>
        </table>
      </contents>
    </form>
  </contents>
</dataScope>

Notice that there is now a special section above the data columns which is differentiated in appearance from the other cells. The column header stamp is stamped across the top of the table, once per data column. Of course, we would prefer that the column header stamps contain labels appropriate to the content of the columns, instead of just showing the same text in every cell. For this we will need to use a new attribute on the table to databind the column header, the columnHeaderData:

<dataScope ... >
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        <!-- all the row DataObjects used by our table for tableData -->
        <demoRowData ... />
        ...

        <!-- DataObjectList to provide information to the column header stamps -->
        <demoColumnHeaderData headerText="First Header"/>
        <demoColumnHeaderData headerText="Second Header"/>  
      </inline>
    </data>
  </provider>

  <contents>
    <form name="testForm">
      <contents>
        <table data:columnHeaderData="demoColumnHeaderData@demoTableData"
               ... >
          <contents>
           ...
          </contents>

          <!-- add a column header stamp node -->
          <columnHeaderStamp>
            <text data:text="headerText"/>
          </columnHeaderStamp>
        </table>
      </contents>
    </form>
  </contents>
</dataScope>

Now, each header has its own unique text. To accomplish this, we did three things:

  1. We created a new DataObjectList, whose size is equal to the number of columns and whose DataObjects each contain the text to display in a column header, stored under the "headerText" key.
  2. We registered that the table's columnHeaderData comes from the databound attribute whose value is the "demoColumnHeaderData" DataObjectList under the named DataObject "demoTableData". This allows the table to use our new DataObjectList to supply data to the header stamps.
  3. Finally, we bound the value of our text column header stamp to come from the "headerText" key of the current DataObject. When the column header stamps are rendered, the individual DataObjects in the column header data are each in turn made the current DataObject when the stamp is rendered above each header. This causes the text to change from column header to column header.

This state of affairs will be adequate for many tables. However, some tables need to allow the users to sort the data items by some criterion, and also need to indicate when the rows are currently sorted. For this purpose, UIX Components supplies a specific column header stamp that meets this need, the SortableHeaderBean. This bean is described in a later section.

Row Headers

In some tables, you will want to label each individual data row with an accompanying label. To achieve this, the table bean offers the rowHeaderStamp. This stamp is similar to the column header stamp. Consider the following example which illustrates the use of a row header stamp:

<dataScope ... >
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        ...

        <!-- DataObjectList to provide information to the row header stamps -->
        <demoRowHeaderData headerText="1"/>
        <demoRowHeaderData headerText="2"/>  
        <demoRowHeaderData headerText="3"/>
      </inline>
    </data>
  </provider>

  <contents>
    <form ... >
      <contents>
        <table ... 
               data:rowHeaderData="demoRowHeaderData@demoTableData" >
          <contents>
           ...
          </contents>

          <!-- row header stamp node -->
          <rowHeaderStamp>
            <text data:text="headerText"/>
          </rowHeaderStamp>
        </table>
      </contents>
    </form>
  </contents>
</dataScope>

The mechanism for adding a row header is the same, only with slightly different child and attribute names: rowHeaderStamp named child and rowHeaderData table attribute. Note, though, that we haven't provided four DataObjects of row header data in our example. This is to illustrate that even without data, the header stamp will render for a row, although its result may be less than desired.

Editable Table Cells

By design, many tables have stamps that render controls that can be edited by the user. The idea is that the user will enter data into the cells of the table which will then be submitted back to the server for processing.

For example, suppose we want to render an editable form input element in every cell of a table column, so that users can enter or change data in those cells. Normally, this can be achieved in a page outside of a table via standard form submissions. Any form input widget has an associated name attribute, which is sent to the server along with the value of the input element when the form is submitted. This is how the server is informed of the values on a page.

As mentioned before, any widget, even an input element, can be used as a column stamp in a table. But if the widget is stamped multiple times down the cells of a column, how is the unique value in each cell sent to the server? Wouldn't this cause each input element to be rendered multiple times with the same name attribute, which would not be the desired HTML?

The answer is that the table fixes this through a process called "name transformation." In essence, the table treats the name attribute on any of its stamps as special, and alters it before rendering the stamp into a given cell. For instance, if a column stamp was an input control with the name "foo", the table would alter the actual value of the name rendered into each cell to take the form:

tableName:foo:rowIndex

where "tableName" would be replaced by the value of the table's name attribute and the "rowIndex" would be replaced with an integer indicating which row that stamp is being rendered into. This means that an input element named "foo", rendered in a table named "myTestTable" in the third row would get the name:

myTestTable:foo:3

In the following example, we've altered our table to put textInput elements in the first column instead of text nodes. If you view the HTML source for this page, you will see that the table has transformed the names of the generated text fields in each row as described above.

<dataScope ... >
  <provider>
   ...
  </provider>

  <contents>
    <form name="testForm">
      <contents>
        <table name="myTestTable"
               ... >
          <contents>
            <textInput data:text="firstColumnText" name="foo"/>
            <button data:text="secondColumnText" destination="http://www.example.org"/>
          </contents>
        </table>
      </contents>
    </form>
  </contents>
</dataScope>

When the form containing this table is submitted, either as the result of a navigation bar link or any other form-triggering action, the server will now receive four name/value pairs representing the values in the textInput column:

Form Control Name Initial Value
myTestTable:foo:0 First row
myTestTable:foo:1 Second row
myTestTable:foo:2 Third row
myTestTable:foo:3 Fourth row

UIX also provides utility classes for retrieving these values on the server after they have been submitted. The class oracle.cabo.data.ServletRequestDataSet may be used to retrieve the values from a ServletRequest. And the class oracle.cabo.servlet.ui.data.PageEventFlattenedDataSet may be used with a UIX Controller PageEvent. Each of these classes implements a DataObjectList of all the input elements in a given table. The length of this list is the number of rows in the table. Each DataObject in this list corresponds to a row in the table. Use the name of an input element, as the key (with each DataObject) to get the value of that element (on the respective row). The following example implements an event handler that pulls out all the table input element values and concatenates them.

public static EventResult doSubmitEvent(BajaContext bc, Page page,
                                        PageEvent event)
{
  // create a new FlattenedDataSet for the table "myTestTable"
  DataSet tableInputs = new PageEventFlattenedDataSet(event,
                                                      "myTestTable");
  StringBuffer s = new StringBuffer(40);
  // this would be the number of rows in the table
  int sz = tableInputs.getLength();
  for(int i=0; i<sz ;i++)
  {
    // get the DataObject representing all the input elements on the current
    // table row.
    DataObject row = tableInputs.getItem(i);
    // get the value of the input element named "foo". we can safely use
    // null for the RenderingContext here:
    Object value = row.selectValue(null, "foo");
    s.append(value);
  }

  EventResult result = new EventResult(page);
  result.setProperty("case", "submit");

  // store the concatenation of the values of all the "foo" elements on the
  // EventResult
  result.setProperty("result", s);
  return result;
}

This automatic name transformation is sometimes undesirable; for example, if you wanted to render a radio button group vertically down a column, then you would have to ensure that each radio element had the same name (so that they belong in the same group and are mutually exclusive). Currently, the only way to do this is to turn off the automatic name transformation and handle name transformations privately by data binding each name attribute. Automatic name transformation is turned off by setting the nameTransformed attribute of the table to false (or Boolean.FALSE in Java).

<table ...
       nameTransformed="false">
 ...
</table>

If you would still like to use PageEventFlattenedDataSet (or ServletRequestDataSet) to get at your data on the server, then you should use oracle.cabo.ui.data.FlattenedDataSet to transform all the table input elements. To get the transformation of input element "foo" on row 5 of table "myTestTable" use:

String newName = FlattenedDataSet.getFlattenedName("myTestTable", 5, "foo");

To get the transformation of your special radio group column with name "radio" use:

String newName = FlattenedDataSet.getFlattenedName("myTestTable", "radio");

One final step is needed to make everything work correctly. The proxied attribute of the table must be set to true (this attribute is explained in a later section). Now you can use PageEventFlattenedDataSet in the usual way to get at the table input element values. To get at the value of your special radio group column use:

  DataSet tableInputs = new PageEventFlattenedDataSet(event,
                                                      "myTestTable");
  Object radioValue = tableInputs.selectValue(null, "radio");

Record Navigation

While we have seen how to create a table with a few rows in it, it is often the case that the data sets in applications will often have large numbers of items. Clearly, a table listing the employees in a large corporation cannot show all records at the same time. For this purpose, we use record navigation to break such a table into more manageable pieces.

With only a bit of additional data, the table bean can render a navigation area which indicates that the current data rows are part of a larger whole. Each row in the table is assigned an index number, and the user is told which row numbers are in view, and which are not currently visible. To render this navigation area, you should supply the table with a few additional attributes:

It is not necessary to supply all four attributes to the table if they are not known. Providing even one of them will cause the table to render a navigation area, and it will default those attributes without explicit values. As an example, here is our demo table with some additional navigation properties which indicate that there is more data than that currently onscreen.

<dataScope ... >
  <provider>
   ...
  </provider>

  <contents>
    <table value="5"
           maxValue="50"
           blockSize="4"  
           ...  >
      <contents>
       ...
      </contents>
    </table>
  </contents>
</dataScope>

In our example, we tell the table that we are viewing rows starting at row 5 (out of a total of 50) and we want to view at most 4 rows at a time. Although we don't give a minimum value, it is assumed to be "1".

It is important to note that the navigational bar is purely cosmetic; it does not control how many rows are actually rendered, or which row is rendered first. Regardless of the values of the value and blockSize attributes, the table will always render as many rows as there are DataObjects in the tableData attribute.

Navigation on the Server

When the navigational links are used, the table generates a UIX Controller event named UIConstants.GOTO_EVENT, or goto in uiXML (see UIX Controller for details about events). This event has three parameters which are described in the table below:

Event Parameter UIConstant Description
source SOURCE_PARAM This parameter identifies the table that generated the event. The value is the "name" attribute of the table.
value VALUE_PARAM This parameter identifies the set of rows that the user is currently viewing. It is set to be the index of the first row in the set.
size SIZE_PARAM This is the size of the set of rows that is currently being viewed. This is usually the blockSize of the table, except in the case when an edge set of rows is being viewed.

On the server, we need to create a new DataObjectList that contains only the table rows that are requested. This means that we need to start with the row identified by the value parameter and only have as many rows as the size parameter. This is done with the help of the class oracle.cabo.ui.data.PagedDataObjectList.

public class TableDemo  {
  public static EventResult doGotoEvent(BajaContext bc, Page page,
                                        PageEvent event)
  {
    // if this is a "goto" event, then we need to get the "value" parameter to
    // figure out what our start index is. If this is not a "goto" event, then
    // we want to start at index "1"
    String valueParam = ((event!=null) && 
                         UIConstants.GOTO_EVENT.equals(event.getName()))
      ? event.getParameter(UIConstants.VALUE_PARAM)
      : "1";

    // the "value" parameter starts at "1"; however, our data is zero based,
    // so adjust the offset
    int value = Integer.parseInt(valueParam)-1;
    DataObjectList tableData = new PagedDataObjectList(_TABLE_DATA, 
                                                       _BLOCK_SIZE.intValue(),
                                                       value); //start index

    // in a more efficient implementation, we would not use DictionaryData;
    // instead, we would implement our own DataObject
    DictionaryData data = new DictionaryData();
    // we need to add one here, since our data is zero based, but the table
    // start index must start at 1
    data.put("value", new Integer(value+1));
    data.put("size", _BLOCK_SIZE);
    data.put("maxValue", new Integer(_TABLE_DATA.getLength()));
    data.put("current", tableData);

    EventResult result = new EventResult(page);
    result.setProperty("tableData", data);
    return result;
  }

  // we want to render at most 30 rows on a single page
  private static final Integer _BLOCK_SIZE = new Integer(30);
  private static final DataObjectList _TABLE_DATA = _sCreateTableData();

  /**
   * this method creates all the static table data.
   */
  private static DataObjectList _sCreateTableData()
  {
    int sz = 94;
    Object[] data = new Object[sz];
    for(int i=1; i<=sz; i++)
    {
      data[i-1] = "Test Data "+i;
    }
    return new ArrayDataSet(data, "firstColumnText");
  }
}

In the above example, we first start by getting the value parameter; this is the index of the first row in the current view of the table. We need to make an offset adjustment since the DataObjectList index is zero based, but the value parameter starts at one.

A PagedDataObjectList is created with the block size that we want (30) and the current start index. Finally, we put this DataObjectList into a DataObject. The DataObject implements the keys value, size, maxValue and current which must be bound respectively to the value, blockSize, maxValue and tableData attributes of the table. This is done by the following uiXML:

<table data:tableData="current@tableData@ctrl:eventResult"
       data:value="value@tableData@ctrl:eventResult"
       minValue="1"
       data:maxValue="maxValue@tableData@ctrl:eventResult"
       data:blockSize="size@tableData@ctrl:eventResult"
       name="table1"
       ... >
 ...
</table>

The above example uses several degrees of indirection in the data binding. See Data Binding for more details about the data binding.

Navigation URL

In the above examples, all the navigation links point back to the current page. In some applications, it might be necessary to direct navigation requests to some other URL. This can be done by setting a destination attribute on the table. This destination must be a well-formed URL of a server that can accept the event parameters we listed. If a destination is provided, the table will render the record navigation links in the page so that they send URL parameters with the necessary record navigation event, to the destination specified. An example is given below:

<table destination="http://www.example.org/eventHandler"
       ...  >
   ...
</table>

Another way to send table events to the server is to specify that the table uses HTML forms for its communication. Although the end result of using forms is actually identical to using a destination URL -- namely, the server will still receive a set of key and value pairs indicating the user's action -- using forms has an additional benefit. If forms are used to submit table actions like record navigation, all other values of the form will be sent to the server along with the table navigation parameters.

As an example, suppose that a table control and a textInput both exist on the same page, and inside the same HTML form. When the the user clicks on the table navigation bar link to move to a new set of rows, it might also be desirable to send the value of the textInput to the server, so that its contents might be preserved and redisplayed when the new page is generated for the user. To achieve this, simply set the table's formSubmitted attribute to be true. Doing so will cause the table to submit the form in which it resides via Javascript -- along with all the other values in that form -- when the user clicks on a navigation area link.

But how, then, does the table send the server the four necessary parameters indicating which record navigation event the user chose? It achieves this by rendering four special, hidden fields into the HTML form in which the table also resides. Before the form is submitted by the table, Javascript is used to change the values of these hidden fields to indicate which action the user took. Here's an example of using form submission:

<dataScope ... >
  <provider>
   ...
  </provider>

  <contents>
    <form name="testForm">
      <contents>
        <table formSubmitted="true"
               ... >
          <contents>
           ...
          </contents>
        </table>
      </contents>
    </form>
  </contents>
</dataScope>

Form submission is also desirable when form input elements are used as column stamps of a table

Empty Tables

There is another special case for record navigation: tables which for some reason or other have no rows to currently display. One example is a table that shows search results for a given term, but which has no data rows because the term searched for contains no matches. For such tables, it is helpful to the user to display a message indicating the reason why no data appears. This text message can be set with the "alternateText" attribute, as shown below:

<dataScope ... >
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        <!-- no rows in this example! -->

        ...
      </inline>
    </data>
  </provider>

  <contents>
    <table data:tableData="demoRowData@demoTableData"
           ...
           alternateText="(No search results were found)">

      ...
    </table>
  </contents>
</dataScope>

The alternate text message will only be displayed if there is no "tableData" set on the table, or if the tableData happens to return a row count of zero.

Sortable Column Headers

Column headers are made sortable by using a SortableHeaderBean as the columnHeaderStamp. However, the sortability of a given column will not make itself apparent until we supply some additional information.

In order for the SortableHeaderBean to render in a way that indicates that it is sortable, we have to give it a value for the sortable attribute. There are three possible values to give this attribute:

uiXML UIConstant Description
yes SORTABLE_YES Indicates that this column is sortable, but is not currently sorted. The column header will then render in such a way as to indicate that the user should select it should he or she wish to sort the data items based on the values of that column.
ascending SORTABLE_ASCENDING Indicates that the column header should render as sortable, and display a notification to the user that the table is already sorted on this column. It also indicates that the sorted column values increase when read down the column.
descending SORTABLE_DESCENDING Indicates that the column header should render as sortable, and notify the user that the table is already sorted on this column. The renderer will signify that the sorted column values decrease when read down the column.

We can try out this capability in our demo by making one of our columns sortable and the other sortable (ascending). Notice how databinding the "sortable" attribute of our new SortableHeaderBean to these values changes the appearance of the column header stamp.

<dataScope ... >
  <provider>
    <data name="demoTableData">
      <inline>
        <!-- all the row DataObjects used by our table for tableData -->
        <demoRowData ... />
        ...

        <!-- DataObjectList to provide information to the column header stamps -->
        <demoColumnHeaderData ... sortValue="yes"/>
        <demoColumnHeaderData ... sortValue="ascending"/>  
      </inline>
    </data>
  </provider>

  <contents>
    <form name="testForm">
      <contents>
        <table ... >
          <contents>
           ...
          </contents>

          <!-- add a sortable column header stamp node -->
          <columnHeaderStamp>
            <sortableHeader data:text="headerText"
                            data:sortable="sortValue"/>
          </columnHeaderStamp>
        </table>
      </contents>
    </form>
  </contents>
</dataScope>

Sorting on the Server

When sortable headers are clicked, the table generates a UIX Controller event with the name UIConstants.SORT_EVENT (or "sort" in uiXML). This event is accompanied by three parameters which are listed in the table below:

Event Parameter UIConstant Description
source SOURCE_PARAM This identifies the table generating this event. This would be the name of the respective table.
value VALUE_PARAM This will be whatever is provided as the value attribute of the SortableHeaderBean for that column header cell. The default value would be the zero-based index of the column.
state STATE_PARAM Indicates whether or not the column header (identified by the value parameter) is already sorted. If it is already sorted, then this parameter would be either the UIConstant SORTABLE_ASCENDING or SORTABLE_DESCENDING depending on which way the column is sorted. If the column is not already sorted, nothing will be sent as the state value.

In the following sections, an example on how to handle sorting on the server is presented. This example is more complicated than it has to be, simply because it is designed to work in a general environment.

The first step is to provide a value attribute for the SortableHeaderBean. We have decided to use the same key that is used for the column data, as the value of each column header. This makes sense because on the server we can get at the value parameter and use it as the key with our table row DataObjects to get at the column data which must be sorted. The following uiXML illustrates this part (it has three sortable columns: name, age and blood):

<table ... >
  <columnHeaderData>
   <col text="Name" sort="yes" value="name"/>
   <col text="Age" sort="yes" value="age"/>
   <col text="Blood Group" sort="yes" value="blood"/>
   <col text="Phone"/>
  </columnHeaderData>
  <columnHeaderStamp>
   <sortableHeader data:text="text"
     data:value="value" ... >
   </sortableHeader>
  </columnHeaderStamp>
  <contents>
    <text data:text="name"/>
    <text data:text="age"/>
    <text data:text="blood"/>
    <text data:text="phone"/>
  </contents>
</table>

The next step is to handle the sort event on the server. The following code pulls out the value and state parameters and puts them on the EventResult:

public static EventResult doSortEvent(BajaContext bc, Page page,
                                      PageEvent event)
{
  EventResult result = new EventResult(page);
  result.setProperty(UIConstants.VALUE_PARAM,
                     event.getParameter(UIConstants.VALUE_PARAM));

  // if we are already sorting in ascending order, then we want to sort in
  // descending order. Otherwise, sort in ascending order
  Object state = event.getParameter(UIConstants.STATE_PARAM);
  result.setProperty(UIConstants.STATE_PARAM,
                     UIConstants.SORTABLE_ASCENDING.equals(state)
                     ? UIConstants.SORTABLE_DESCENDING
                     : UIConstants.SORTABLE_ASCENDING);
  return result;
}

Now we have to create a DataObject that would determine the sortable attribute for each column header. The following DataObject must be called with the current column header's value attribute as the key. It then checks to see if this is the column header that has been sorted, by comparing key to the value on the EventResult (this was set in the event handler above). If it is indeed the right column header, then we return the state (either ascending or descending); otherwise, we return null.

private static final DataObject _SORT_COLUMN_HEADER = new DataObject() {
    public Object selectValue(RenderingContext rc, Object key)
    {
      BajaContext bc = BajaRenderingContext.getBajaContext(rc);
      EventResult er = EventResult.getEventResult(bc);
      if (er!=null)
      {
        // check to see if it is this column that has been sorted. We assume
        // that "key" is the "value" attribute of this column header.
        if (key.equals(er.getProperty(UIConstants.VALUE_PARAM)))
          return er.getProperty(UIConstants.STATE_PARAM);
      }
      return null;
    }
  };

The next step would be to databind the sortable attribute of the SortableHeaderBean to use the above DataObject. We have done this databinding in an unusual way; we first want to use the above DataObject to get a value, and failing that we want to get the value from the appropriate DataObject for this column in columnHeaderData. The following uiXML code achieves this objective:

<table ... >
  <columnHeaderData>
   <col text="Name" sort="yes" value="name"/>
   <col text="Age" sort="yes" value="age"/>
   <col text="Blood Group" sort="yes" value="blood"/>
   <col text="Phone"/>
  </columnHeaderData>
  <columnHeaderStamp>
   <sortableHeader data:text="text"
     data:value="value">
    <boundAttribute name="sortable">
     <defaulting>
      <!-- first try to get a value from the sortColumnHeader
         dataObject. This dataObject is passed whatever the 
         value key is bound to (in the current columnHeaderData) -->
      <dataObject data:select="value" 
                  source="sortColumnHeader"/>
      <dataObject data:source="(value)@sortColumnHeader"/>
      <!-- if the above boundValue returns null, then try to get
           a value from the columnHeaderData  -->
      <dataObject select="sort"/>
     </defaulting>
    </boundAttribute>
   </sortableHeader>
  </columnHeaderStamp>

  ...            
</table>

The boundAttribute element is another way of databinding the sortable attribute of sortableHeader. The defaulting boundValue tries to get a value from its first child, and failing that, returns the value from the second child. In the above case, the first child tries to get a value from the sortColumnHeader data provider (let's assume that this data provider is bound to the _SORT_COLUMN_HEADER we created above), and the second child uses the sort key to get the default sortable state from a DataObject in columnHeaderData.

What have we achieved so far? The sortable headers are not only clickable, but each time they are clicked, they toggle between the display sorted in ascending order and the display sorted in descending order. However, the columns are still not sorted; the next section will implement the actual sorting.

public static DataObject getSortedTableData(RenderingContext rc,
                                            String namespace, String name)
{
  DataObject[] data;
  BajaContext bc = BajaRenderingContext.getBajaContext(rc);
  EventResult er = EventResult.getEventResult(bc);
  if (er!=null)
  {
    // we need to clone because we are going to mutate the array:
    data = (DataObject[]) _TABLE_DATA.clone();
    Object state = er.getProperty(UIConstants.STATE_PARAM);
    Object key = er.getProperty(UIConstants.VALUE_PARAM);
    Comparator comp = new DataObjectComparator(
         rc,
         key, // this is the key to sort the DataObjects on
         UIConstants.SORTABLE_ASCENDING.equals(state));
    Arrays.sort(data, comp);
  }
  else
    data = _TABLE_DATA;

  return new ArrayDataSet(data);
}

The above data provider returns a DataObjectList that is sorted according to the state and value that was set on the EventResult by the event handler doSortEvent(...). _TABLE_DATA is the unsorted data. The actual sorting is done by the java.util.Arrays class. The other interesting class is DataObjectComparator:

private static final class DataObjectComparator implements Comparator
{
  public DataObjectComparator(RenderingContext context,
                              Object key, boolean ascending)
  {
    _context = context;
    _key = key;
    _ascending = ascending;
  }

  public int compare(Object o1, Object o2)
  {
    DataObject dob1 = (DataObject) o1;
    DataObject dob2 = (DataObject) o2;

    Comparable val1 = (Comparable) dob1.selectValue(_context, _key);
    Object val2 = dob2.selectValue(_context, _key);
    int comp = val1.compareTo(val2);

    // if we are not sorting in ascending order, then negate the comparison:
    return _ascending ? comp : -comp;
  }

  private final RenderingContext _context;
  private final Object _key;
  private final boolean _ascending;
}

The above class is charged with comparing two DataObjects, and uses _key to get values from both. It assumes the values are Comparable and does the comparison (negating the result if we are sorting in descending order).

Selection

The ability to select certain rows of a table for further processing is a very useful feature for many applications. The Table supports selection via the tableSelection child. Two selection beans are offered with UIX: SingleSelectionBean and MultipleSelectionBean. The former is used in tables where only a single row may be selected; the latter is used when multiple rows may be selected. Before we discuss these beans it must be noted that the Table must be used inside a form in order for these beans to work correctly. This is because these beans use form elements to indicate their selection to the server.

Single Selection

Single selection in a table is achieved by using the SingleSelectionBean as the tableSelection child. Here is a simple example:

<table name="table1" ... >
  <tableSelection>
   <singleSelection/>
  </tableSelection>
  
  ...
</table>

Notice that initially none of the rows are marked as being selected. An initial selection may be set by using the selectedIndex attribute of singleSelection to the (zero based) index of the row that must be selected:

<singleSelection selectedIndex="1" />

The next step is to let the user know what he/she can do with his/her selection. This can be done by using the text attribute of singleSelection. It is also necessary to add submitButtons to the layout, so that the user may click on them when he/she wants to begin processing; the buttons may be added as indexed children to the selection bean, as in the following example:

<tableSelection>
 <singleSelection text="Select record and ..."
                  selectedIndex="1" >
  <contents>
   <submitButton text="Copy" ctrl:event="copy" />
   <submitButton text="Delete" ctrl:event="delete" />
  </contents>
 </singleSelection>
</tableSelection>

How do we handle selection on the server? You may have noticed that the above submitButtons both trigger events on the server. Here is the event handler (the point to note here is the use of oracle.cabo.ui.beans.table.SelectionUtils to get at the selected index):

public static EventResult doSelectionEvent(BajaContext bc, Page page,
                                           PageEvent event)
{
  DataObject tableRows = new PageEventFlattenedDataSet(event, "table1");
  int index = SelectionUtils.getSelectedIndex(tableRows);
  String name = "Nothing Selected";
  // make sure that something was selected:
  if (index>=0)
  {
    DataObject row = _TABLE_DATA.getItem(index);
    name = row.selectValue(null, "name").toString();
  }
  EventResult result = new EventResult(page);
  result.setProperty("action", event.getName());
  result.setProperty("name", name);
  return result;
}

Multiple Selection

Multiple selection in a table is achieved by using the MultipleSelectionBean as the tableSelection child. Similar to singleSelection, multipleSelection also supports the text attribute and indexed children. Here is an example:

<table ... >
  <tableSelection>
   <multipleSelection text="Select record and ...">
    <contents>
     <submitButton text="Copy" ctrl:event="copy"/>
     <submitButton text="Delete" ctrl:event="delete" />
    </contents>
   </multipleSelection>
  </tableSelection>

  ...
</table>

It is possible to set initial selections by data binding the selected and selection attributes of multipleSelection. Each time the MultipleSelectionBean is stamped down a column, it will evaluate its selected attribute. During this time, the corresponding row in the selection DataObjectList will be the current DataObject:

<multipleSelection data:selected="selectedKey" ...>
 <selection>
  <!-- create a dataObjectList, each dataObject has a selectedKey
       whose value is either true or false -->
  <row selectedKey="true"/> <!-- select the first row -->
  <row selectedKey="false"/>
  <row selectedKey="true"/> <!-- select the third row -->
  
  ...
 </selection>

 ...
</multipleSelection>

In the above example, we set the value of the selection attribute inline, but this could easily have been databound (as we have seen before).

Multiple selection is handled on the server using the SelectionUtils.getSelectedIndices(...) method. This method returns an array, each element of which is an index of a row that was selected. The following is the event handler code:

public static EventResult doMultiSelectEvent(BajaContext bc, Page page,
                                             PageEvent event)
{
  PageEventFlattenedDataSet tableRows = 
    new PageEventFlattenedDataSet(event, "table1");
  int[] indices = SelectionUtils.getSelectedIndices(tableRows);
  DataObjectList resultTableData = new SelectedList(_TABLE_DATA, indices);

  EventResult result = new EventResult(page);
  result.setProperty("action", event.getName());
  result.setProperty("tableData", resultTableData);
  return result;
}

The SelectedList class is a useful utility class; it takes a DataObjectList and an array of selected indices and implements a DataObjectList that only contains the DataObjects that were selected:

private static final class SelectedList implements DataObjectList
{
  public SelectedList(DataObjectList data, int[] selectedIndices)
  {
    _data = data;
    _indices = selectedIndices;
  }

  // returns the number of selected rows
  public int getLength()
  {
    return _indices.length;
  }

  // gets the selected row at the given index.
  public DataObject getItem(int index)
  {
    return _data.getItem(_indices[index]);
  }

  private final DataObjectList _data;
  private final int[] _indices;
}

multipleSelection has a "select all/none" feature. Typically, this means select (or deselect) the table rows that are visible in the current record set. However, in some applications, this operation must be performed on all the rows of a record set (not just the ones that are currently visible). In order to facilitate this, multipleSelection adds a form parameter named "selectMode". The value of this parameter is empty, if the user has not clicked on select-all/none. If the user clicks on select-all, the value of this parameter would be "all". If select-none was clicked on, the value would be "none". The event handling code on the server can use this parameter to determine if the user clicked on select-all/none.

Disabling Selection

Both singleSelection and multipleSelection support a disabled attribute. When the selection bean is stamped down a column, the disabled attribute will be evaluated and if this returns Boolean.TRUE, then the selection feature for that row will be disabled (The DataObject for the current table row will be the current DataObject):

<dataScope>
  <provider>
    <data name="tableData">
     <inline>
      <row name="Person 1" age="11" disabledKey="true"/>
      <row name="Person 2" age="12" disabledKey="false"/>
      <row name="Person 3" age="13" disabledKey="false"/>
      <row name="Person 4" age="14" disabledKey="true"/>
     </inline>
    </data>
  </provider>

  <contents>
   <form name="form1">
    <contents>
     
     <table data:tableData="row@tableData" ... >
       <tableSelection>
        <multipleSelection ...
          data:disabled="disabledKey">
        </multipleSelection>
       </tableSelection>

       ...
     </table>
    </contents>
   </form>
  </contents>
</dataScope>

Detail-Disclosure

In some applications, a table row only displays a summary of properties, for a given element. In these cases it would be nice to have a way of clicking on a button and opening up a view with all the details pertinent to that row. The detail-disclosure feature of the TableBean provides this functionality.

Detail-disclosure is turned on by setting the detail named child of the table. This child is just another stamp; except, it is rendered only when the user has asked for the details of a row. Therefore, this child may show all the specifics of the current row. Here is an example:

<table ... >
  <detail>
   <!-- this is the detailed stamp -->
   <labeledFieldLayout>
    <contents>
    Name
    <styledText data:text="name" styleClass="OraDataText"/>
    Age
    <styledText data:text="age" styleClass="OraDataText"/>
    Blood Group
    <styledText data:text="blood" styleClass="OraDataText"/>
    Phone
    <styledText data:text="phone" styleClass="OraDataText"/>
    </contents>
   </labeledFieldLayout>
  </detail>

  <columnHeaderData>
   <col text="Name"/>
   <col text="Age" />
  </columnHeaderData>
  <contents>
    <!-- these are the regular column stamps -->
    <text data:text="name"/>
    <text data:text="age"/>
  </contents>
</table>

In the above example, the regular table displays only the Name and Age properties of each row. When the user asks for details, the detail stamp is rendered, providing additional information such as Blood Group and Phone (Notice that the detail child is rendered with the current table row DataObject as the current DataObject; so the data binding works the same way as for a column stamp).

In the above example, however, none of the detail sections were disclosed. So it was not possible to see a detail stamp rendered. The disclosure state of each arrow is controlled by the detailDisclosure attribute of the table. This must be a DataObjectList with a DataObject for each row of the table. Each DataObject is queried with the key UIConstants.DISCLOSED_KEY (or disclosed in uiXML). If the result of this query is Boolean.TRUE then the detailed information for that row will be disclosed. The following is an example:

<table ... >
  ...

  <detailDisclosure>
   <row disclosed="false"/>
   <row disclosed="false"/>
   <row disclosed="true"/> <!-- disclose the third row -->
   <row disclosed="false"/>
   <row disclosed="false"/>
   <row disclosed="true"/> <!-- disclose the sixth row -->
  </detailDisclosure>
</table>

In the previous examples the detail-disclosure arrows don't actually do anything; the examples are purely cosmetic. Let's see how we can make an interactive demo. When an arrow is clicked a UIX Controller event is generated: either UIConstants.SHOW_EVENT or HIDE_EVENT ("show" or "hide" in uiXML) depending on whether the details must be shown or hidden (respectively). The event has the following two parameters:

Parameter UIConstant Description
source SOURCE_PARAM Identifies the table that generated this event. This would be the table's name attribute.
value VALUE_PARAM Identifies the row that must be disclosed (or undisclosed). This is a zero based index.

The value parameter makes it easy to know which row must be disclosed/undisclosed. However, we don't have enough information to know what rows have already been disclosed. If we want to preserve the current disclosed state of the table, then we need to add some more state to the client-side page. In this example we will add a hidden form parameter named "disclosed" to the detail section (notice that now the table must be used in form submitted mode):

<table formSubmitted="true" ... >
 ...
 <detail>
  <labeledFieldLayout>
   <contents>
   ...
   <formValue name="disclosed" value="1"/>
   </contents>
  </labeledFieldLayout>
 </detail>
</table>

In the following event handler, we get the index of the row that the user chose and decide whether we want to disclose (or undisclose) depending on the event name:

public static EventResult doHideShowEvent(BajaContext bc, Page page,
                                          PageEvent event)
{
  PageEventFlattenedDataSet tableRows = 
    new PageEventFlattenedDataSet(event, "table1");

  // this is the row that must be (un)disclosed:
  int row = Integer.parseInt(event.getParameter(UIConstants.VALUE_PARAM));
  // decide whether we want to disclose or undisclose depending on the name
  // of the event
  boolean disclose = UIConstants.SHOW_EVENT.equals(event.getName());

  DataObjectList detailData = new DetailData(tableRows, row, disclose);

  EventResult result = new EventResult(page);
  result.setProperty("detailData", detailData);
  return result;
}

In the above code we create a PageEventFlattenedDataSet which contains the current disclosure state of the table. We use it to create a detailData DataObjectList. This list encompasses the previous disclosure state of the tree and new state. This is then pushed onto the EventResult so that it may be accessed from the uiXML code. The class DetailData is presented below:

private static final class DetailData implements DataObjectList
{
  /**
   * @param pageEvent contains the current disclosure state of the table
   * @param index the index of the row that must have its disclosure state
   * changed
   * @param disclosure the new disclosure state for the row
   */
  public DetailData(DataObjectList pageEvent, int index, boolean disclose)
  {
    _pageEvent = pageEvent;
    // initially, none of the table rows will be disclosed, so there will be
    // no pageEvent data and this length would be zero:
    _length = pageEvent.getLength();
    _index = index;
    _disclose = disclose;
  }

  public int getLength()
  {
    // make sure that the length we return is sufficiently large enough that
    // we reach the index we want to change
    return (_index >= _length) ? _index+1 : _length;
  }

  public DataObject getItem(int index)
  {
    boolean disclose;
    if (index==_index)
    {
      // this is the index that we want to change.
      disclose = _disclose;
    }
    else if (index < _length)
    {
      // this index can safely be pulled from the pageEvent
      DataObject row = _pageEvent.getItem(index);
      // if there was a "disclosed" form element on this row then we
      // consider the row disclosed:
      disclose = (row.selectValue(null, "disclosed") != null);
    }
    else
      disclose = false;

    return disclose ? _DISCLOSE_TRUE : _DISCLOSE_FALSE;
  }

  private final DataObjectList _pageEvent;
  private final int _index, _length;
  private final boolean _disclose;

  private static final DataObject _DISCLOSE_TRUE = new DataObject() {
      public Object selectValue(RenderingContext rc, Object key)
      {
        return Boolean.TRUE;
      }
    };

  private static final DataObject _DISCLOSE_FALSE = new DataObject() {
      public Object selectValue(RenderingContext rc, Object key)
      {
        return Boolean.FALSE;
      }
    };
}

Basically, what this class does, is use the table's current disclosure state to determine which rows have been disclosed and return the appropriate DataObject for true (if disclosed) or false (if undisclosed). If the index is the index we want to change, then the new disclosure state is returned. The only tricky thing here is to remember that PageEventFlattenedDataSet only has data in it, if there were form input elements in the table. In the initial state, nothing is disclosed, so there will not be any input elements rendered, and the FlattenedDataSet would return zero as its length. Therefore, we need to handle this special case carefully.

Creating Column Footers

There are some cases where it is appropriate to summarize the data in a table in a special section located at the bottom of the data area. This is known as the column footer, and it, too is a named child of the table. However, unlike the column header stamp and row header stamp, the column footer is not a stamp. It is rendered only once at the bottom of the table instead of being stamped across every column. This is by design; most column footers summarize table-wide statistics or actions rather than per-column actions. In fact, there are two special UINodes that are intended to be used only as column footers: addTableRow and totalRow. Let's examine them both.

AddTableRow Bean

First up is addTableRow. This column footer node is intended for use in tables that need to allow the user to add additional data rows at runtime. You can customize the number of rows to add, the text it displays, and alter its URL destination. All will default if not specified, however. Here is an example of one in use:

<table ... >
  ...

  <!-- column footer node -->
  <columnFooter>
   <addTableRow rows="5"/>
  </columnFooter>
</table>

Although this example prompts the user to add 5 rows, without that rows attribute one new row will be requested when the bean is activated. Also, this is another table customization which requires that events be sent to the server. Rows are not added on the fly; the page must be regenerated with the additional rows included, which means that the server must be notified and update its data source. Here are the (now surprisingly familiar) parameters which are sent to the server on a UIConstants.ADD_ROWS_EVENT (or "addRows" in uiXML) event:

Parameter UIConstant Description
source SOURCE_PARAM Once again, the source is the name of the table.
size SIZE_PARAM The number of rows requested to be added.

TotalRow Bean

Similar to the previous bean is the totalRow. Instead of adding rows, this bean is used to show the total for numerical columns in a table. Note that, at this time, the bean itself does not calculate totals; rather, it will display any totals calculated by the server or by Javascript in its properly formatted controls.

The totalRow serves two purposes: it renders a button in the column footer allowing the user to update the total and it allows controls to be rendered under each column to hold the total value for that column. The button is rendered automatically, with text that can be configured using the bean's text attribute. Activating the button will once again send an event to the server; this time, the event is UIConstants.UPDATE_EVENT (or "update" in uiXML).

Parameter UIConstant Description
source SOURCE_PARAM The source is the name of the table.

Unlike addTableRow, totalRow allows indexed children, which it uses as the holders of the total values for each column. The first indexed child of the totalRow will be placed under the rightmost column of the table, with subsequent indexed children being placed to the left of the previous ones. Thus, the indexed children will appear under each column from right to left as they are added, as shown in the following example:

<table ... >
  ...

  <!-- column footer node -->
  <columnFooter>
    <totalRow>
      <contents>
        <textInput name="total" text="42" columns="5"/>
      </contents>
    </totalRow>
  </columnFooter>
</table>

Again, at this time it is up to the table creator to actually perform the updates to any total indexed children nodes when the update button is activated.

Sometimes it is necessary to have both a totalRow and an addTableRow in a table. This can be done by adding the totalRow as an indexed child of the addTableRow, as in the following example:

<table ... >
  ...

  <columnFooter>
   <addTableRow>
    <contents>
     <!-- addTableRow beans may have at most a single child,
          and this child can only be a totalRow bean -->
     <totalRow>
       <contents>
         <textInput name="total" text="400" />
       </contents>
     </totalRow>
    </contents>
   </addTableRow>
  </columnFooter>
</table>

Formatting the Table Content

Up until now, we've been concerned only with the actual content of the table cells and a few actions that communicate with the server. Sometimes it is necessary to change the appearance or format of our table in order to clarify the structure of the data, or just to make it easier to read. There are many ways to customize a table format, perhaps a dizzying array of combinations. Fortunately, the mechanism for achieving these different customizations is consistent.

The easiest way to change the appearance of a table is to change its width, which can be done just by setting the width attribute on the table. Although the width can be specified either by pixels or percentages of the width of the parent element, percentages are commonly preferred. Note, though, that no matter what value is specified as the desired table width, if it is not large enough to accommodate the content it will be overridden at display time to take up the minimum amount of necessary space.

Let's move to the alignment of the content cells. According to the user interface designs, different types of content should be aligned differently to make them easier for users to understand. This can be achieved using a new set of DataObjects used for formatting. In this case, since alignment should be uniform down the cells of a given column, we will use the table's columnFormat attribute. Like many other data-driven table attributes, this one is also a DataObjectList. In this case, each DataObject in the list corresponds to one of the columns, from left to right in the table layout. Specifying formats inside any of these DataObjects causes the corresponding column to change its appearance accordingly. Let's take a look at a first example:

<dataScope ... >
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        ...

        <!-- DataObjectList to provide column formatting -->
        <demoColumnFormats/>
        <demoColumnFormats columnDataFormat="numberFormat"/>  
        <demoColumnFormats columnDataFormat="iconButtonFormat"/>
      </inline>
    </data>
  </provider>

  <contents>
    <form ... >
      <contents>
        <table ...
               width="100%"
               data:columnFormats="demoColumnFormats@demoTableData">
          <contents>
            <!-- the first column stamp, a text node -->
            <textInput data:text="firstColumnText" name="foo"/>
            <!-- a second column stamp, a static text node -->
            <text text="42"/>
            <!-- the third column stamp, a button -->
            <button data:text="secondColumnText" destination="http://www.example.org"/>
          </contents>

          ...
        </table>
      </contents>
    </form>
  </contents>
</dataScope>

We changed a few things in this example, so let's explore them in turn. We set the table to take "100%" width. Then, we added another column stamp to the table -- a text node -- to give us more to work with. Next, we created a new DataObjectList like before, but we called this one "demoColumnFormats". The DataObjects in this list will provide formatting to each of the three columns, in turn. Inside these DataObjects, we put some formatting data, which we will describe in depth soon. Finally, we set a new attribute on the table itself, columnFormats, which points to our new DataObjectList.

Let's look more closely at the the DataObjects used to format our table. We've already indicated that they supply the formatting information, but how does this work? Unlike other DataObjects that store values under arbitrary keys chosen by the implementor, formatting objects must store their values under known, publicized keys. When rendering, the table will ask each of these column format DataObjects for the values stored under each of these public keys. If the DataObject returns a value for that key, it is used to change the column format; if not, the table defaults that particular format.

In our example, the format DataObjects have stored a value under the "columnDataFormat" String, one of the well known keys. This key determines the alignment of the content in the column based on what type of content is claimed to be inside it. The first DataObject does not return any value for that key, which causes it to default its alignment to that of plain text content, or to the left. The second DataObject provides the value "numberFormat" when queried. Since the specifications dictate that numbers should be right-aligned, the data cells in this table column are aligned accordingly. Finally, the third column returns "iconButtonFormat" for its data type, which currently causes its content to be center-aligned, the proper alignments for columns of icons or buttons. This should be clear when you view the previous example.

Column Formats

There are four keys that column format DataObjects can return values for. Let's see an example that illustrates them all:

<dataScope ... >
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        ...

        <!-- DataObjectList to provide column formatting -->
        <demoColumnFormats cellNoWrapFormat="true"/>
        <demoColumnFormats columnDataFormat="numberFormat"
                           displayGrid="false"/>  
        <demoColumnFormats columnDataFormat="iconButtonFormat"
                           width="100%"/>
      </inline>
    </data>
  </provider>

  <contents>
   ...
  </contents>
</dataScope>

Using this example as a guide, we can summarize all of the possibilities for formatting on a column format DataObject:

Key
UIConstant
Description
columnDataFormat
COLUMN_DATA_FORMAT_KEY
This key is used to specify the type of content being displayed in the table column because, unfortunately, the table is not smart enough to figure this out for itself. Different data formats cause changes in alignment for the data cells:
  • textFormat: the default format, used for most column stamps and used to contain text data. Currently, it causes left-alignment.
  • numberFormat: this format should be specified for any column whose data cells contain numbers, as it causes them to right-align, keeping the numbers in an easily read line.
  • iconButtonFormat: icons and buttons should get center alignment, and this format value allows it to happen.
cellNoWrapFormat
CELL_NO_WRAP_FORMAT_KEY
Sometimes it is preferable to not allow the contents of a cell to wrap to multiple lines. If this is desired for a given data column, simply make its column format DataObject return "true" (Boolean.TRUE) when queried with this key. This is not the default behavior because disallowing wrapping can cause tables to become too wide, and is thus discouraged.
width
WIDTH_KEY
Used to indicate what percentage of extra width in the table this particular column should take. If any table takes up more space than its content needs, it will apportion that space among its columns. However, some columns (like buttons) don't need any more space than the minimum they are given. Other columns, like those containing text, should take extra space if it allows them to reduce the number of lines needed to display their content. Column format DataObjects can take percentage values, like "50%" or "100%" to indicate how much of the extra space that column should receive. The total among all the columns should add to 100%.
displayGrid
DISPLAY_GRID_KEY
By default, a grid line is shown to the left of every column. In some cases, you may want to use vertical grid lines more sparingly to emphasize the relationship between some columns and de-emphasize it between others. If you wish to turn off a vertical grid line located before, or to the left, of a given data column, just return "false" when that column is queried with the this key.

It is important to note that all of these format values are hints, not hard and fast rules. Some rendering looks or platforms may not support all formatting values, so don't be surprised if a given rule doesn't work in some agents. Formatting should never be considered critical to an application, as it is not depdendable.

Just as columns can control where vertical grid lines appear, it is also possible to customize where horizontal grid lines appear for any given row. This is done using a "rowFormats" attribute on the table, as shown in the following example:

<dataScope ... >
  <provider>
    <!-- all the data used by our table demo -->
    <data name="demoTableData">
      <inline>
        ...

        <!-- DataObjectList to provide row formatting information -->
        <demoRowFormats/>
        <demoRowFormats displayGrid="false"/>  
      </inline>
    </data>
  </provider>

  <contents>
    <form ... >
      <contents>
        <table ...
               data:rowFormats="demoRowFormats@demoTableData">
          ...
        </table>
      </contents>
    </form>
  </contents>
</dataScope>

In this example, we've added a row format DataObject which requests that no horizontal grid be shown above the second row. As you can see, it works, although it is rare that this functionality should ever be used.

Also available for rare cases are columnHeaderFormats and rowHeaderFormats, which are also DataObjectLists that can be set as attributes on the table itself. The only key that these list DataObjects respond to is the "cellNoWrapFormat" key, previously mentioned. Each DataObject corresponds to a column or row header and controls its ability to wrap its content. Again, the default is to allow wrapping, as not doing so can cause the table to grow too wide for the user's taste. These formatting attributes should be used sparingly.

Banding

The final type of formatting we will examine here is banding. Banding refers to the ability to alternate colors of cells or rows in a table so that the content of those cells or columns is grouped visually. There are two ways to control banding. The first, easiest, and most common is to specify table-wide banding using a tableFormat DataObject. This single formatting object applies to the entire table, which is why it is a DataObject instead of a DataObjectList.

By default tables have no banding, but if a tableFormat DataObject is set on the table, it will be queried with the key "tableBanding" (UIConstants.TABLE_BANDING_KEY). Should the DataObject return the well-known value "columnBanding" (UIConstants.COLUMN_BANDING), alternating columns of the table will have different background colors, as demonstrated in this example:

<dataScope ... >
  <provider>
    <data name="demoTableData">
      <inline>
        ...

        <!-- DataObject for table-wide formatting (i.e. banding) -->
        <demoTableFormat tableBanding="columnBanding"/>
      </inline>
    </data>
  </provider>

  <contents>
    <form ... >
      <contents>
        <table ...
               data:tableFormat="demoTableFormat@demoTableData">
          ...
        </table>
      </contents>
    </form>
  </contents>
</dataScope>

Similarly, returning the value "rowBanding" (UIConstants.ROW_BANDING) will cause rows of the table to alternate their background colors, as the next example shows. You can also return an integer when queried with the "bandingInterval" (UIConstants.BANDING_INTERVAL_KEY) key to control the number of rows in each alternating band.

<dataScope xmlns="http://xmlns.oracle.com/uix/ui"
           xmlns:data="http://xmlns.oracle.com/uix/ui">
  <provider>
    <data name="demoTableData">
      <inline>
        ...

        <!-- DataObject for table-wide formatting (i.e. banding) -->
        <demoTableFormat tableBanding="rowBanding"/>
      </inline>
    </data>
  </provider>

  <contents>
    ...
  </contents>
</dataScope>

Column banding, however, can also be achieved in arbitrary patterns. This allows individual columns to take the light banding column without using a regular alternation pattern. To achieve this affect, simply have one or more of the column format DataObjects (described above) return either "light" (BANDING_SHADE_LIGHT) or "dark" (BANDING_SHADE_DARK) when queried with the "bandingShade" (UIconstants.BANDING_SHADE_KEY) key. In the next example, only the third column will get a light band color:

<dataScope ... >
  <provider>
    <data name="demoTableData">
      <inline>
        ...

        <!-- DataObjectList to provide column formatting -->
        <demoColumnFormats cellNoWrapFormat="true"/>
        <demoColumnFormats columnDataFormat="numberFormat"
                           displayGrid="false"/>  
        <demoColumnFormats columnDataFormat="iconButtonFormat"
                           width="100%"
                           bandingShade="light"/>

        <!-- DataObjectList to provide row formatting information -->
        <demoRowFormats/>
        <demoRowFormats displayGrid="false"/>  

        <!-- DataObject for table-wide formatting (i.e. banding) -->
        <demoTableFormat tableBanding="rowBanding"/>
      </inline>
    </data>
  </provider>

  <contents>
    ...
  </contents>
</dataScope>

Note that if banding is explicitly set for any column format object, the table-wide formatting will be completely ignored. It is not permissible to mix table-wide banding with explicit column banding, as this would produce odd coloration patterns.

The TableStyle class

On final note about formatting: the class oracle.cabo.ui.beans.table.TableStyle provides a convenient way for Java programmers to set most of these formatting options. For example, to create a format for a column with numbers and no grid use:

DataObject columnFormat = new TableStyle(TableStyle.HIDE_GRID_MASK |
                                         TableStyle.NUMBER_FORMAT_MASK);

Encapsulated Columns

In the previous section we've discussed setting column stamps, setting columnHeaderStamps, binding columnHeaderData and setting columnFormats. Each of these properties involved setting some global attribute on the table. It would be nice if we had a way to set all the properties (pertinent to a single column) in a single location. The ColumnBean helps you do just that. Here is an example of using it:

<table ... >
  <contents>
    ...

    <column>
     <columnFormat columnDataFormat="iconButtonFormat"
        bandingShade="light" width="50%" displayGrid="false"/>
     <columnHeader>Third Column</columnHeader>
     <contents>
      <!-- This is the column stamp -->
      <button data:text="secondColumnText"/>
     </contents>
    </column>

  </contents>
</table>

The first thing to note is that the column element has been set as a column stamp in the table. It supports a named child columnHeader (which is similar to the columnHeaderStamp in the table) which is used as the stamp for this column's header. A columnHeaderData attribute is supported in case it is necessary to data bind the stamp. Both columnFormat and columnHeaderFormat are supported to allow the column to be formatted.

It is also worth noting that the column element works in harmony with the other attributes of the table (as the above example demonstrates). However, any attribute set on the column will overwrite the corresponding attribute on the table.

Client-Side Action

By using JavaScript, certain Table operations can be performed on the client-side. All of these operations require the TableProxy JavaScript object. In order to use this object, the table's proxied attribute must be set to Boolean.TRUE:

<table proxied="true">
 ...
</table>

Reading/Writing Input Elements

The following is an example of using the TableProxy to read table elements. In it we sum up all the values in a column and update the total. The following is the uiXML code:

<dataScope>
  <provider>
    <data name="tableData">
     <inline>
      <row name="Person 1" cost="11" />
      <row name="Person 2" cost="12" />
      <row name="Person 3" cost="13" />
      <row name="Person 4" cost="14" />

      ...
     </inline>
    </data>
  </provider>

  <contents>
   <form name="form1">
    <contents>
     <table proxied="true" data:tableData="row@tableData"
            name="table1" ... >
       <contents>
         <text data:text="name"/>
         <textInput name="cost" data:text="cost"/>
       </contents>
       <columnFooter>
        <totalRow destination="javascript:updateTotal();">
         <contents>
          <textInput name="total" text="50"/>
         </contents>
        </totalRow>
       </columnFooter>
     </table>
    </contents>
   </form>
  </contents>
</dataScope>

When the update button is clicked, the updateTotal() JavaScript method is called. This method creates a new TableProxy using the table name, gets the number of rows in the table by calling the getLength() method, gets the form element in the column (for each row), sums the values and writes the total to the appropriate text field. Notice the use of the getFormElement(...) method; this method takes an element name and the row index, and returns that element.

function updateTotal()
{
 var proxy = new TableProxy('table1');
 var rowCount = proxy.getLength(); 
 var total = 0;
 for(var i=0; i<rowCount; i++)
 {
  var currTextField = proxy.getFormElement('cost', i);
  // the minus zero here is necessary to convert the string value
  // to a number
  total += currTextField.value - 0;
 }
 document.form1.total.value = total;
}

Working with Selection

When a table is used in single selection mode, it is possible to use the TableProxy's getSelectedRow() method to get the index of the currently selected row. Here is an example:

function hire()
{
 var proxy = new TableProxy('table1');
 var row = proxy.getSelectedRow();
 // check to make sure that something was selected
 if (row < 0)
 {
  alert('You have not chosen anyone!');
 }
 else
 {
  var name = proxy.getFormElement('theName', row).value;
  alert('You have chosen to hire '+name);
 }
}

The above function is called by the following uiXML:

<table name="table1" ...>
  <tableSelection>
   <singleSelection ...>
    <contents>
     <button text="Hire" destination="javascript:hire()"/>
    </contents>
   </singleSelection>
  </tableSelection>
  <contents>
    <text data:text="name"/>
    <text data:text="cost"/>
    <formValue name="theName" data:value="name"/>
  </contents>
</table>

Similarly, use the getSelectedRows() method on the TableProxy to get the selected row indices in multiple selection mode. This method returns an array, each element of which is an index of a selected row. Here is some sample JavaScript code:

function recruit()
{
 var proxy = new TableProxy('table1');
 var rows = proxy.getSelectedRows();
 var length = rows.length;
 // make sure that something was selected
 if (length > 0)
 {
  var list = "";
  // loop through each selected row and concatenate the name
  for(var i=0; i < length; i++)
  {
   // get the next selected row index
   var row = rows[i];
   // get the selected row (from the index) and pull out the name
   var name = proxy.getFormElement('theName', row).value;
   list += '\n'+name;
  }
  alert("You have chosen to recruit "+list);
 }
 else
 {
  alert("You have not chosen anyone to recruit!");
 }
}

This method would be called from the following uiXML:

<table ... >
  <tableSelection>
   <multipleSelection text="Select ...">
    <contents>
     <button text="Recruit" destination="javascript:recruit()"/>
    </contents>
   </multipleSelection>
  </tableSelection>

  ...
</table>

Conclusion

This chapter talked about the TableBean and how to use it to display tabular data and implement record navigation, sorting, selection, detail-disclosure and totaling. For more information, see the JavaDoc for the TableBean or consult the uiXML Element Reference for the <table> element.