UI Components

Tables

The basics of table creation were already mentioned in the chapter binding of a table. This section details the definition of table structure and the Container PMO.

Definition of the Table Structure with a Row PMO

The structure of the table is defined in a PMO class, whose instances represent rows in the table. Thus, these classes are called "row PMOs".

Row PMO classes are standard PMOs, which do not represent sections and thus shouldn’t be annotated with @UISection.

Here, the annotation UI element defines the column in the table. A column showing the name of a contact, for instance, can be setup as follows:

    @UITableColumn(flexGrow = 10)
    @UILabel(position = 10, label = "Name")
    public String getName() {
        return contact.getName();
    }

That means every property that was found in the row PMO class provided by the ContainerPmo is set up as a column. Every defined property specifies the setup of the column as well as the concrete field inside the table’s cell. Next to all aspects that are provided by @UITableColumn it takes the label from the field definition.

The properties of a table column are described using the properties in the row PMO. But these properties cannot be bound to any specific row PMO instance (there might be none if the table is empty or many if there are multiple rows). Hence all bindings are directed to the row PMO class. That also means: If you want to translate a column label the key is created using the class name of the row PMO.
@UITableColumn

By design all properties of a PMO are presented in the table. With the annotation @UITableColumn additional properties of a table column could be bound.

Column size

You can use the properties width (in pixels) and flexGrow to modify the column size. The flexGrow specifies what amount of the available space inside the table the column should take up and grow (if resized), proportionally to the other columns. If flexGrow is set to 0, the column has a fixed width.

Collapsible columns

The property collapsible can make a column collapsible, i.e. the user can toggle the visibility of a column. It also defines if a collapsible column should be initally collapsed.

This feature only works if the table is created inside of a section. If there are any collapsible columns in the table, a right aligned header menu is displayed in the created section with which the user can toggle the visibility of a collapsible column.

Setting the visibility of a column programmatically after the GridSection has already been created, should be done using GridSection#setColumnVisible(String columnKey, boolean visible) instead of Grid#getColumnByKey() to make sure, the checked state of its MenuItem is set correctly!
Sortable columns

The property sortable allows the column to be sorted. The type of the column value must implement Comparable, which defines the order in which the values are sorted.

Model bindings (modelAttribute) can not be used with sortable columns, a getter method must be used.
Text alignment

It is possible to customize text alignment inside of individual columns by setting the textAlign value of the @UITableColumn annotation. The alignment also applies to the header and footer of the column.

Container PMO

In addition to a row PMO, a container PMO is needed to provide the row PMOs that should be rendered in the table. The container PMO can be optionally annotated with @UISection to display the table in a TableSection. For most cases, the standard implementation SimpleTablePmo can be used as super class.

SimpleTablePmo

Most commonly, the row PMO objects are converted from a list of model objects. In this case, the abstract class org.linkki.core.defaults.columnbased.pmo.SimpleTablePmo<MO, ROW> may be extended. It defines a constructor that requires the list or the supplier for the list of model objects. Additionally the method createRow(MO) needs to be implemented. This method simply takes a model object and creates a row PMO for it and is only called once for every model object. A simple example may look like this:

@UISection
public class SimpleContactTablePmo extends SimpleTablePmo<Contact, ContactRowPmo> {

    private final Consumer<Contact> editAction;
    private final Consumer<Contact> deleteAction;

    public SimpleContactTablePmo(List<Contact> contacts, Consumer<Contact> editAction, Consumer<Contact> deleteAction) {
        super(contacts);
        this.editAction = editAction;
        this.deleteAction = deleteAction;
    }

    @Override
    protected ContactRowPmo createRow(Contact contact) {
        return new ContactRowPmo(contact, editAction, deleteAction);
    }
}

The SimpleTablePmo is an abstract convenience implementation of the ContainerPmo<ROW> interface.

ContainerPmo Interface

In general, a container PMO have to implement the interface ContainerPmo<ROW>. The type parameter ROW is the row PMO class that defines the table structure.

The method List<ROW> getItems() is called by the ContainerBinding to add the elements to the table. It should always return the same instance of List<ROW> as long as the items do not change. The SimpleItemSupplier offers support for that.

By overriding the default method int getPageLength() the number of rows shown can be controlled. By default 15 rows are shown. It is a common practice to allow tables to 'grow' to a certain size and then limit the number of lines while also enabling the scrolling for the table. If 0 is returned the table grows dynamically with the content, without limit.

    @Override
    public int getPageLength() {
        return 0;
    }

The column structure of the table is determined by the row PMO class, which is returned by the method Class<? extends ROW> getItemPmoClass. In the default implementation the class of the generic parameter ROW is returned. To support tables which are configured with other components for the cells, the method can be overwritten and return a subclass of ROW.

If the table should support the adding of items, the default method Optional<ButtonPmo> getAddItemButtonPmo must be overwritten. How a ButtonPmo is created is described in the chapter ButtonPmo.

SimpleItemSupplier

The SimpleItemSupplier<PMO, MO> is used to only create a new List<PMO>, if a row was changed. When using ContainerPmo interface directly, SimpleItemSupplier should be used if the displayed rows may change dynamically.

The instantiating is done with two parameters

  • modelObjectSupplier of type Supplier<List<MO>> is called to access a list of the model objects

  • mo2pmoMapping of type Function<MO, PMO> is called for the creation of a PMO for a model object

Example initializing of a SimpleItemSupplier
    public ContactTablePmo(List<Contact> contacts, Consumer<Contact> editAction, Consumer<Contact> deleteAction) {
        items = new SimpleItemSupplier<>(() -> contacts,
                p -> new ContactRowPmo(p, editAction, deleteAction));
    }

Hierarchical Tables

Sometimes, the data in a table should be grouped for presentation, for example when summarizing values over certain categories. In that case, the data represents a tree-like structure with parent-child-relationships between rows. The resulting table will be a Vaadin TreeTable which allows collapsing and showing the child-rows of a row.

A hierarchical table
Figure 1. Hierarchical table

This can be realized by using org.linkki.core.defaults.columnbased.pmo.HierarchicalRowPmo<PMO>, row PMOs that contain further rows as children. It is possible to use multiple subclasses for row PMOs, using HierarchicalRowPmo only for collapsible rows. To indicate that the table contains hierarchical rows, the ContainerPmo should return true in the method isHierarchical(). By default, this method returns true if getItemPmoClass() returns a class that implements HierarchicalRowPmo, which means that all rows are collapsible.

public abstract class SummarizingPersonRowPmo extends AbstractPersonRowPmo
        implements HierarchicalRowPmo<AbstractPersonRowPmo> {

    private final List<? extends AbstractPersonRowPmo> childRows;

    public SummarizingPersonRowPmo(List<? extends AbstractPersonRowPmo> childRows) {
        this.childRows = childRows;
    }

    @Override
    public List<? extends AbstractPersonRowPmo> getChildRows() {
        return childRows;
    }

If the order of the rows might change due to user input, you should use a SimpleItemSupplier as with the ContainerPmo to avoid recreating the PMOs for unchanged rows.

    public CategoryRowPmo(Supplier<Stream<Player>> playerStreamSupplier,
            Function<Stream<Player>, Stream<CMO>> playersToChildModelObjectMapper,
            Function<CMO, CPMO> childModelObject2pmoMapping) {
        this.playerStreamSupplier = playerStreamSupplier;
        childRowSupplier = new SimpleItemSupplier<>(
                () -> playersToChildModelObjectMapper.apply(playerStreamSupplier.get()).collect(Collectors.toList()),
                childModelObject2pmoMapping);
    }

Row selection in Tables

By default, table rows are not selectable. If table row selection should be possible, the ContainerPmo implementation should also implement the interface SelectableTablePmo. This interface requires three methods:

  • ROW getSelection(): returns the selected row

  • setSelection(ROW): sets the new selected row

  • onDoubleClick(): executes action when a double click is made on the selected row. It is safe to assume that setSelection(ROW) is already called.

Note that it is not possible to nullify the selection once a row is selected.

Example for a selectable table
public class PlaygroundSelectableTablePmo extends SimpleTablePmo<TableModelObject, PlaygroundRowPmo>
        implements SelectableTablePmo<PlaygroundRowPmo> {

    public static final String NOTIFICATION_DOUBLE_CLICK = "Double clicked on ";
    private PlaygroundRowPmo selected;

    // ...

    @Override
    public PlaygroundRowPmo getSelection() {
        return selected;
    }

    @Override
    public void setSelection(PlaygroundRowPmo selectedRow) {
        this.selected = selectedRow;
    }

    @Override
    public void onDoubleClick() {
        Notification.show(NOTIFICATION_DOUBLE_CLICK + selected.getModelObject().getName());
    }

}
Selectable table
Figure 2. Example result

By overriding the default method getFooterPmo() a footer row is generated. The implementation of the interface TableFooterPmo must implement the method getFooterText(String column).

The parameter column is the ID of the column for which the text should be displayed. An example for this would be a sum of all items from a column.

    private final TableFooterPmo footer;

    public CarTablePmo(List<Car> carStorage, Handler addCarAction) {
        this.addCarAction = addCarAction;
        this.items = new SimpleItemSupplier<>(() -> carStorage, CarRowPmo::new);

        this.footer = c -> calculateTotalRetention(c, carStorage);
    }

    @Override
    public Optional<TableFooterPmo> getFooterPmo() {
        return Optional.of(footer);
    }

    private String calculateTotalRetention(String column, List<Car> cars) {
        switch (column) {
            case Car.PROPERTY_RETENTION:
                return new DecimalFormat("#,##0.00", DecimalFormatSymbols.getInstance(UiFramework.getLocale()))
                        .format(cars.stream()
                                .mapToDouble(Car::getRetention)
                                .sum());

            case Car.PROPERTY_CAR_TYPE:
                return "Total Retention:";

            default:
                return "";
        }
    }
The ContainerPMO can be annotated with @BindStyleNames and LinkkiApplicationTheme#GRID_FOOTER_SUM to make the table footer text bold and right aligned. This is particularly useful for right aligned number columns which have a footer to display the sum.

ButtonPmo

Currently the ContainerPmo provides a method getAddItemButtonPmo(), by which a plus button can be added besides the name of the table.