The Table
component is intended for presenting tabular
data organized in rows and columns. The Table
is one of
the most versatile components in IT Mill Toolkit. Table cells can include text
or arbitrary UI components. You can easily implement editing of the table data,
for example clicking on a cell could change it to a text field for editing.
The data contained in a Table
is managed using the Data
Model of IT Mill Toolkit (see Chapter 7, Data Model), through
the Container
interface of the
Table
. This makes it possible to bind a table directly
to a data souce such as a database query. Only the visible part of the table
is loaded into the browser and moving the visible window with the scrollbar loads
content from the server. While the data is being loaded, a tooltip will be
displayed that shows the current range and total number of items in the
table. The rows of the table are items in the container
and the columns are properties. Each table row (item) is
identified with an item identifier (IID), and each column
(property) with a property identifier (PID).
When creating a table, you first need to define columns with
addContainerProperty()
. This method comes in two
flavours. The simpler one takes the property ID of the column and uses it also
as the caption of the column. The more complex one allows differing PID and
header for the column. This may make, for example, internationalization of
table headers easier, because if a PID is internationalized, the
internationalization has to be used everywhere where the PID is used. The
complex form of the method also allows defining an icon for the column from a
resource. The "default value" parameter is used when new properties (columns)
are added to the table, to fill in the missing values. (This default has no
meaning in the usual case, such as below, where we add items after defining
the properties.)
/* Create the table with a caption. */ Table table = new Table("This is my Table"); /* Define the names and data types of columns. * The "default value" parameter is meaningless here. */ table.addContainerProperty("First Name", String.class, null); table.addContainerProperty("Last Name", String.class, null); table.addContainerProperty("Year", Integer.class, null); /* Add a few items in the table. */ table.addItem(new Object[] {"Nicolaus","Copernicus",new Integer(1473)}, new Integer(1)); table.addItem(new Object[] {"Tycho", "Brahe", new Integer(1546)}, new Integer(2)); table.addItem(new Object[] {"Giordano","Bruno", new Integer(1548)}, new Integer(3)); table.addItem(new Object[] {"Galileo", "Galilei", new Integer(1564)}, new Integer(4)); table.addItem(new Object[] {"Johannes","Kepler", new Integer(1571)}, new Integer(5)); table.addItem(new Object[] {"Isaac", "Newton", new Integer(1643)}, new Integer(6));
In this example, we used an increasing Integer
object
as the Item Identifier, given as the second parameter to
addItem()
. The actual rows are given simply as object
arrays, in the same order in which the properties were added. The objects must
be of the correct class, as defined in the
addContainerProperty()
calls.
Scalability of the Table
is largely dictated by
the container. The default IndexedContainer
is
relatively heavy and can cause scalability problems, for example, when
updating the values. Use of an optimized application-specific
container is recommended. Table does not have a limit for the number
of items and is just as fast with hundreds of thousands of items as
with just a few. With the current implementation of scrolling, there
is a limit around 500 000 rows, depending on the browser and the pixel
height of rows.
The Table
allows selecting one or more items by
clicking them with the mouse. When the user selects an item, the IID of the
item will be set as the property of the table and a
ValueChangeEvent
is triggered. To enable selection,
you need to set the table selectable. You will also
need to set it as immediate in most cases, as we do
below, because without it, the change in the property will not be
communicated immediately to the server.
The following example shows how to enable the selection of items in a
Table
and how to handle
ValueChangeEvent
events that are caused by changes
in selection. You need to handle the event with the
valueChange()
method of the
Property.ValueChangeListener
interface.
/* Allow selecting items from the table. */ table.setSelectable(true); /* When an item is selected, the selection is sent immediately to server. */ table.setImmediate(true); /* Feedback from selection. */ final Label current = new Label("Selected: -"); /* Handle selection change. */ table.addListener(new Property.ValueChangeListener() { public void valueChange(ValueChangeEvent event) { current.setValue("Selected: " + table.getValue()); } });
If the user clicks on an already selected item, the selection will
deselected and the table property will have null
value. You can disable this behaviour by setting
setNullSelectionAllowed(false)
for the table.
A table can also be in multiselect mode, where a user
can select and unselect any item by clicking on it. The mode is enabled
with the setMultiSelect()
method of the
Select
interface of
Table
. Selecting an item triggers a
ValueChangeEvent
, which will have as its parameter
an array of item identifiers.
.i-table {} .i-table-header-wrap {} .i-table-header {} .i-table-header-cell {} .i-table-resizer {} /* Column resizer handle. */ .i-table-caption-container {} .i-table-body {} .i-table-row-spacer {} .i-table-table {} .i-table-row {} .i-table-cell-content {}
Notice that some of the widths and heights in a table are calculated dynamically and can not be set in CSS.
The Table.CellStyleGenerator
interface allows
you to set the CSS style for each individual cell in a table. You need
to implement the getStyle()
, which gets the
row (item) and column (property) identifiers as parameters and can
return a style name for the cell. The returned style name will be
concatenated to prefix "i-table-cell-content-
".
Alternatively, you can use a
Table.ColumnGenerator
(see Section 4.10.4, “Generated Table Columns”) to generate the actual UI
components of the cells and add style names to them. A cell style
generator is not used for the cells in generated columns.
Table table = new Table("Table with Cell Styles"); table.addStyleName("checkerboard"); // Add some columns in the table. In this example, the property IDs // of the container are integers so we can determine the column number // easily. table.addContainerProperty("0", String.class, null, "", null, null); // Row header for (int i=0; i<8; i++) table.addContainerProperty(""+(i+1), String.class, null, String.valueOf((char) (65+i)), null, null); // Add some items in the table. table.addItem(new Object[]{"1", "X", "X", "X", "X", "X", "X", "X", "X"}, new Integer(0)); table.addItem(new Object[]{"2", "P", "P", "P", "P", "P", "P", "P", "P"}, new Integer(1)); for (int i=2; i<6; i++) table.addItem(new Object[]{String.valueOf(i+1), "", "", "", "", "", "", "", ""}, new Integer(i)); table.addItem(new Object[]{"7", "P", "P", "P", "P", "P", "P", "P", "P"}, new Integer(6)); table.addItem(new Object[]{"8", "X", "X", "X", "X", "X", "X", "X", "X"}, new Integer(7)); table.setPageLength(8); // Set cell style generator table.setCellStyleGenerator(new Table.CellStyleGenerator() { public String getStyle(Object itemId, Object propertyId) { int row = ((Integer)itemId).intValue(); int col = Integer.parseInt((String)propertyId); // The first column. if (col == 0) return "rowheader"; // Other cells. if ((row+col)%2 == 0) return "black"; else return "white"; } });
You can then style the cells, for example, as follows:
/* Center the text in header. */ .i-table-header-cell { text-align: center; } /* Basic style for all cells. */ .i-table-checkerboard .i-table-cell-content { text-align: center; vertical-align: middle; padding-top: 12px; width: 20px; height: 28px; } /* Style specifically for the row header cells. */ .i-table-cell-content-rowheader { background: #E7EDF3 url(../default/table/img/header-bg.png) repeat-x scroll 0 0; } /* Style specifically for the "white" cells. */ .i-table-cell-content-white { background: white; color: black; } /* Style specifically for the "black" cells. */ .i-table-cell-content-black { background: black; color: white; }
The table will look as shown in the figure below.
The default style for Table
provides a table
with a scrollbar. The scrollbar is located at the right side of the
table and becomes visible when the number of items in the table
exceeds the page length, that is, the number of visible items. You can
set the page length with setPageLength()
.
Setting the page length to zero makes all the rows in a table visible, no matter how many rows there are. Notice that this also effectively disables buffering, as all the entire table is loaded to the browser at once. Using such tables to generate reports does not scale up very well, as there is some inevitable overhead in rendering a table with Ajax. For very large reports, generating HTML directly is a more scalable solution.
The default scrollable style supports most of the table features. User
can resize the columns by dragging their borders, change the sorting
by clicking on the column headers, collapse the columns if
columnCollapsingAllowed
is
true
, and reorder them if
columnReorderingAllowed
is
true
. You can set the column width of
individual columns with setColumnWidth()
.
The cells of a Table
can contain any user
interface components, not just strings. If the rows are higher than
the row height defined in the default theme, you have to define the
proper row height in a custom theme.
When handling events for components inside a
Table
, such as for the
Button
in the example below, you usually need
to know the item the component belongs to. Components do not
themselves know about the table or the specific item in which a
component is contained. Therefore, the handling method must use some
other means for finding out the Item ID of the item. There are a few
possibilities. Usually the easiest way is to use the
setData()
method to attach an arbitrary
object to a component. You can subclass the component and include the
identity information there. You can also simply search the entire
table for the item with the component, although that solution may not
be so scalable.
The example below includes table rows with a
Label
in XHTML formatting mode, a multiline
TextField
, a CheckBox
,
and a Button
that shows as a link.
// Create a table and add a style to allow setting the row height in theme. final Table table = new Table(); table.addStyleName("components-inside"); /* Define the names and data types of columns. * The "default value" parameter is meaningless here. */ table.addContainerProperty("Sum", Label.class, null); table.addContainerProperty("Is Transferred", CheckBox.class, null); table.addContainerProperty("Comments", TextField.class, null); table.addContainerProperty("Details", Button.class, null); /* Add a few items in the table. */ for (int i=0; i<100; i++) { // Create the fields for the current table row Label sumField = new Label(String.format("Sum is <b>$%04.2f</b><br/><i>(VAT incl.)</i>", new Object[] {new Double(Math.random()*1000)}), Label.CONTENT_XHTML); CheckBox transferredField = new CheckBox("is transferred"); // Multiline text field. This required modifying the height of the // table row. TextField commentsField = new TextField(); commentsField.setRows(3); // The Table item identifier for the row. Integer itemId = new Integer(i); // Create a button and handle its click. A Button does not know // the item it is contained in, so we have to store the item // ID as user-defined data. Button detailsField = new Button("show details"); detailsField.setData(itemId); detailsField.addListener(new Button.ClickListener() { public void buttonClick(ClickEvent event) { // Get the item identifier from the user-defined data. Integer itemId = (Integer)event.getButton().getData(); getWindow().showNotification("Link "+itemId.intValue()+" clicked."); } }); detailsField.addStyleName("link"); // Create the table row. table.addItem(new Object[] {sumField, transferredField, commentsField, detailsField}, itemId); } /* Show just three rows because they are so high. */ table.setPageLength(3);
The row height has to be set higher than the default with a style rule such as the following:
/* Table rows contain three-row TextField components. */ .i-table-components-inside .i-table-cell-content { height: 54px; }
The table will look as shown in the figure below.
Normally, a Table
simply displays the items and
their fields as text. If you want to allow the user to edit the
values, you can either put them inside components as we did above, or
you can simply call setEditable(true)
and the
cells are automatically turned into editable fields.
Let us begin with a regular table with a some columns with usual Java
types, namely a Date
,
Boolean
, and a String
.
// Create a table. It is by default not editable. final Table table = new Table(); // Define the names and data types of columns. table.addContainerProperty("Date", Date.class, null); table.addContainerProperty("Work", Boolean.class, null); table.addContainerProperty("Comments", String.class, null); // Add a few items in the table. for (int i=0; i<100; i++) { Calendar calendar = new GregorianCalendar(2008,0,1); calendar.add(Calendar.DAY_OF_YEAR, i); // Create the table row. table.addItem(new Object[] {calendar.getTime(), new Boolean(false), ""}, new Integer(i)); // Item identifier } table.setPageLength(8); layout.addComponent(table);
You could put the table in editable mode right away if you need
to. We'll continue the example by adding a mechanism to switch the
Table
from and to the editable mode.
final CheckBox switchEditable = new CheckBox("Editable"); switchEditable.addListener(new Property.ValueChangeListener() { public void valueChange(ValueChangeEvent event) { table.setEditable(((Boolean)event.getProperty().getValue()).booleanValue()); } }); switchEditable.setImmediate(true); layout.addComponent(switchEditable);
Now, when you check to checkbox, the components in the table turn into editable fields, as shown in Figure 4.22, “A Table in Normal and Editable Mode” below.
The field components that allow editing the values of particular types
are defined in a field factory that implements the
FieldFactory
interface. The default
implementation is BaseFieldFactory
, which
offers the following crude mappings:
Table 4.4. Type to Field Mappings in BaseFieldFactory
Property Type | Mapped to Field Class |
---|---|
Date | A DateField . |
Boolean | A CheckBox . |
Item | A Form . The fields of the
form are automatically created from the item's properties
using the default field factory, that is,
BaseFieldFactory . The normal use
for this property type is inside a
Form and is less useful inside a
Table . |
others | A TextField . The text field
manages conversions from the basic types, if
possible. |
Field factories are covered with more detail in Section 4.15.2, “Binding Form to Data”. In the default
BaseFieldFactory
(you might want to look the
source code), the mappings are defined in
createField(
method, but
you can implement any other of the abstract
Class
type,
Component
uiContext)FieldFactory
methods, depending on your
needs. You could just implement the
FieldFactory
interface, but We recommend that
you extend the BaseFieldFactory
according to
your needs.
As the items in a Table
are not indexed, iterating
over the items has to be done using an iterator. The
getItemIds()
method of the
Container
interface of Table
returns a Collection
of item identifiers over which
you can iterate using an Iterator
. For an example
about iterating over a Table
, please see Section 7.4, “Collecting items in Containers”. Notice that you may not modify the
Table
during iteration, that is, add or remove
items. Changing the data is allowed.
You might want to have a column that has values calculated from other
columns. Or you might want to format table columns in some way, for
example if you have columns that display currencies. The
ColumnGenerator
interface allows defining custom
generators for such columns.
You add new generated columns to a Table
with
addGeneratedColumn()
. It takes the column
identifier as its parameters. Usually you want to have a more
user-friendly and possibly internationalized column header. You can set
the header and a possible icon by calling
addContainerProperty()
before adding the generated column.
// Define table columns. table.addContainerProperty("date", Date.class, null, "Date", null, null); table.addContainerProperty("quantity", Double.class, null, "Quantity (l)", null, null); table.addContainerProperty("price", Double.class, null, "Price (e/l)", null, null); table.addContainerProperty("total", Double.class, null, "Total (e)", null, null); // Define the generated columns and their generators. table.addGeneratedColumn("date", new DateColumnGenerator()); table.addGeneratedColumn("quantity", new ValueColumnGenerator("%.2f l")); table.addGeneratedColumn("price", new PriceColumnGenerator()); table.addGeneratedColumn("total", new ValueColumnGenerator("%.2f e"));
Notice that the addGeneratedColumn()
always
places the generated columns as the last column, even if you defined some
other order previously. You will have to set the proper order with
setVisibleColumns()
.
table.setVisibleColumns(new Object[] { "date", "quantity", "price", "total"});
The generators are objects that implement the
Table.ColumnGenerator
interface and its
generateCell()
method. The method gets the
identity of the item and column as its parameters, in addition to the
table object. It has to return a component object.
The following example defines a generator for formatting
Double
valued fields according to a format string
(as in java.util.Formatter
).
/** Formats the value in a column containing Double objects. */ class ValueColumnGenerator implements Table.ColumnGenerator { String format; /* Format string for the Double values. */ /** Creates double value column formatter with the given format string. */ public ValueColumnGenerator(String format) { this.format = format; } /** * Generates the cell containing the Double value. The column is * irrelevant in this use case. */ public Component generateCell(Table source, Object itemId, Object columnId) { Property prop = source.getItem(itemId).getItemProperty(columnId); if (prop.getType().equals(Double.class)) { Label label = new Label(String.format(format, new Object[] { (Double) prop.getValue() })); // Set styles for the column: one indicating that it's a value and a more // specific one with the column name in it. This assumes that the column // name is proper for CSS. label.addStyleName("column-type-value"); label.addStyleName("column-" + (String) columnId); return label; } return null; } }
If you wish to have a custom style for the cells, you have to set it in
the generator. A CellStyleGenerator
defined for a
table will not be called for the cells of generated columns.
The generator is called for all the visible (or more accurately cached) items in a table. If the user scrolls the table to another position in the table, the columns of the new visible rows are generated dynamically. The columns in the visible (cached) rows are also generated always when an item has a value change. It is therefore usually safe to calculate the value of generated cells from the values of different rows (items).
When you set a table as editable
, regular fields
will change to editing fields. When the user changes the values in the
fields, the generated columns will be updated automatically. Putting a
table with generated columns in editable mode has a few quirks. The
editable mode of Table
does not affect generated
columns. You have two alternatives: either you generate the editing fields
in the generator or, in case of formatter generators, remove the generator
in the editable mode. The example below uses the latter approach.
// Have a check box that allows the user to make the quantity // and total columns editable. final CheckBox editable = new CheckBox("Edit the input values - calculated columns are regenerated"); editable.setImmediate(true); editable.addListener(new ClickListener() { public void buttonClick(ClickEvent event) { table.setEditable(editable.booleanValue()); // The columns may not be generated when we want to have them // editable. if (editable.booleanValue()) { table.removeGeneratedColumn("quantity"); table.removeGeneratedColumn("total"); } else { // In non-editable mode we want to show the formatted values. table.addGeneratedColumn("quantity", new ValueColumnGenerator("%.2f l")); table.addGeneratedColumn("total", new ValueColumnGenerator("%.2f e")); } // The visible columns are affected by removal and addition of // generated columns so we have to redefine them. table.setVisibleColumns( new Object[] { "date","quantity","price","total","consumption", "dailycost" }); } });
You will also have to set the editing fields in
immediate
mode to have the update occur
immediately when an edit field loses the focus. You can set the fields
in immediate
mode with the a custom
FieldFactory
, such as the one given below:
public class ImmediateFieldFactory extends BaseFieldFactory { public Field createField(Class type, Component uiContext) { // Let the BaseFieldFactory create the fields Field field = super.createField(type, uiContext); // ...and just set them as immediate ((AbstractField)field).setImmediate(true); return field; } } ... table.setFieldFactory(new ImmediateFieldFactory());
If you generate the editing fields with the column generator, you avoid having to use such a field factory, but of course have to generate the fields for both normal and editable modes.
Figure 4.23, “Table with Generated Columns in Normal and Editable Mode” below shows a table with columns calculated (blue) and simply formatted (black) with column generators.
You can find the complete generated columns example in the Feature Browser
demo application in the installation package, in
com.itmill.toolkit.demo.featurebrowser.GeneratedColumnExample.java
.