UPDATE-RATE

Die Test-Installation wird aktuell zwischen 8:00 und 18:00 Uhr stündlich zur vollen Stunde aktualisiert.

Documentation

Kitchen Sink documentation of style: 'Delos' of skin: 'ILIAS'

Data

Description

Purpose
The Data Table lists records in a complete and clear manner; the fields of a record are always of the same nature as their counterparts in all other records, i.e. a column has a dedicated shape. Each record is mapped to one row, while the number of visible columns is identical for each row. The purpose of exploration is unknown to the Data Table, and it does not suggest or favor a certain way of doing so.
Composition
The Data Table consists of a title, a View Control Container for (and with) View Controls and the table-body itself. The Table brings some ViewControls with it: The assumption is that the exploration of every Data Table will benefit from pagination, sortation and column selection. Records are being applied to Columns to build the actual cells.
Effect
The ordering among the records in the table, the visibility of columns as well as the number of simultaneously displayed rows are controlled by the Table's View Controls. Operating the order-glyphs in the column title will change the records' order. This will also reflect in the aria-sort attribute of the columns' headers. If the Data Table is provided with an id, the values from its View Controls are stored in the session and re-applied on concecutive calls.

Rivals

Presentation Table
There is a weighting in the prominence of fields in the Presentation Table; Aside from maybe the column' order - left to right, not sortation of rows - all fields are displayed with equal emphasis (or rather, the lack of it).
Listing Panel
Listing Panels list items, where an item is a unique entity in the system, i.e. an identifiable, persistently stored object. This is not necessarily the case for Tables, where records can be composed of any data from any source in the system.

Rules

Usage
  1. Tables MUST NOT be used to merely arrange elements visually; displayed records MUST have a certain consistency of content.
  2. A Data Table SHOULD have at least 3 Columns.
  3. A Data Table SHOULD potentially have an unlimited number of rows.
  4. Rows in the table MUST be of the same structure.
  5. Tables MUST NOT have more than one View Control of a kind, e.g. a second pagination would be forbidden.
Interaction
  1. View Controls used here MUST only affect the table itself.
Accessibility
  1. The HTML tag enclosing the actual tabular presentation MUST have the role-attribute "grid".
  2. The HTML tag enclosing the actual tabular presentation MUST have an attribute "aria-colcount" with the value set to the amount of available cols (as opposed to visible cols!)
  3. The row with the columns' headers and the area with the actual data MUST each be enclosed by a tag bearing the role-attribute "rowgroup".
  4. The HTML tag enclosing one record MUST have the role-attribute "row".
  5. A single cell MUST be marked with the role-attribute "gridcell".
  6. Every single cell (including headers) MUST have a tabindex-attibute initially set to "-1". When focused, this changes to "0".

Example 1: Base

a data table

User ID
success
repeat
Actions
User ID: 867 Login: student1 last login: Thursday, 25.04.2024 progress: success: passed repeat: no Fee: £ 40,00
User ID: 8923 Login: student2 last login: Saturday, 27.04.2024 progress: success: failed repeat: yes Fee: £ 36,79
User ID: 8748 Login: student3_longname last login: Monday, 10.07.2023 progress: success: failed repeat: yes Fee: £ 36,79
User ID: 8750 Login: student5 last login: Sunday, 05.05.2024 progress: success: failed repeat: yes Fee: £ 3,79
User ID: 8751 Login: student6 last login: Friday, 03.05.2024 progress: success: failed repeat: yes Fee: £ 67,00
User ID: 8749 Login: studentAB last login: Sunday, 28.04.2024 progress: success: passed repeat: no Fee: £ 114,00
User ID: 123 Login: superuser last login: Saturday, 04.05.2024 progress: success: failed repeat: yes Fee: £ 0,00
<?php
 
declare(strict_types=1);
 
namespace ILIAS\UI\examples\Table\Data;
 
use ILIAS\UI\Implementation\Component\Table as T;
use ILIAS\UI\Component\Table as I;
use ILIAS\Data\Range;
use ILIAS\Data\Order;
use ILIAS\UI\URLBuilder;
use Psr\Http\Message\ServerRequestInterface;
 
function base()
{
    global $DIC;
    $f = $DIC['ui.factory'];
    $r = $DIC['ui.renderer'];
    $df = new \ILIAS\Data\Factory();
    $refinery = $DIC['refinery'];
    $request = $DIC->http()->request();
 
    /**
     * This is what the table will look like:
     * Columns define the nature (and thus: shape) of one field/aspect of the data record.
     * Also, some functions of the table are set per column, e.g. sortability
     */
    $columns = [
        'usr_id' => $f->table()->column()->number("User ID")
            ->withIsSortable(false),
        'login' => $f->table()->column()->text("Login")
            ->withHighlight(true),
        'email' => $f->table()->column()->eMail("eMail"),
        'last' => $f->table()->column()->date("last login", $df->dateFormat()->germanLong()),
        'achieve' => $f->table()->column()->statusIcon("progress")
            ->withIsOptional(true),
        'achieve_txt' => $f->table()->column()->status("success")
            ->withIsSortable(false)
            ->withIsOptional(true),
        'repeat' => $f->table()->column()->boolean("repeat", 'yes', 'no')
            ->withIsSortable(false),
        'fee' => $f->table()->column()->number("Fee")
            ->withDecimals(2)
            ->withUnit('£', I\Column\Number::UNIT_POSITION_FORE)
            ->withOrderingLabels('cheapest first', 'most expensive first'),
        'failure_txt' => $f->table()->column()->status("failure")
            ->withIsSortable(false)
            ->withIsOptional(true, false),
    ];
 
    /**
     * Define actions:
     * An Action is an URL carrying a parameter that references the targeted record(s).
     * Standard Actions apply to both a collection of records as well as a single entry,
     * while Single- and Multiactions will only work for one of them.
     * Also see the docu-entries for Actions.
    */
 
    /** this is the endpoint for actions, in this case the same page. */
    $here_uri = $df->uri($DIC->http()->request()->getUri()->__toString());
 
    /**
     * Actions' commands and the row-ids affected are relayed to the server via GET.
     * The URLBuilder orchestrates query-paramters (a.o. by assigning namespace)
     */
    $url_builder = new URLBuilder($here_uri);
    $query_params_namespace = ['datatable', 'example'];
 
    /**
     * We have to claim those parameters. In return, there is a token to modify
     * the value of the param; the tokens will work only with the given copy
     * of URLBuilder, so acquireParameters will return the builder as first entry,
     * followed by the tokens.
     */
    list($url_builder, $action_parameter_token, $row_id_token) =
    $url_builder->acquireParameters(
        $query_params_namespace,
        "table_action", //this is the actions's parameter name
        "student_ids"   //this is the parameter name to be used for row-ids
    );
 
    /**
     * array<string, Action> [action_id => Action]
     */
    $actions = [
        'edit' => $f->table()->action()->single( //never in multi actions
            /** the label as shown in dropdowns */
            'Properties',
            /** set the actions-parameter's value; will become '&datatable_example_table_action=edit' */
            $url_builder->withParameter($action_parameter_token, "edit"),
            /** the Table will need to modify the values of this parameter; give the token. */
            $row_id_token
        ),
        'compare' => $f->table()->action()->multi( //never in single row
            'Add to Comparison',
            $url_builder->withParameter($action_parameter_token, "compare"),
            $row_id_token
        ),
        'delete' =>
            $f->table()->action()->standard( //in both
                'Remove Student',
                $url_builder->withParameter($action_parameter_token, "delete"),
                $row_id_token
            )
            /**
             * An async Action will trigger an AJAX-call to the action's target
             * and display the results in a modal-layer over the Table.
             * Parameters are passed to the call, but you will have to completely
             * build the contents of the response. DO NOT render an entire page ;)
             */
            ->withAsync(),
        'info' =>
            $f->table()->action()->standard( //in both
                'Info',
                $url_builder->withParameter($action_parameter_token, "info"),
                $row_id_token
            )
            ->withAsync()
    ];
 
 
 
    /**
     * Configure the Table to retrieve data with an instance of DataRetrieval;
     * the table itself is agnostic of the source or the way of retrieving records.
     * However, it provides View Controls and therefore parameters that will
     * influence the way data is being retrieved. E.g., it is usually a good idea
     * to delegate sorting to the database, or limit records to the amount of
     * actually shown rows.
     * Those parameters are being provided to DataRetrieval::getRows.
     */
    $data_retrieval = new class ($f, $r) implements I\DataRetrieval {
        public function __construct(
            protected \ILIAS\UI\Factory $ui_factory,
            protected \ILIAS\UI\Renderer $ui_renderer
        ) {
        }
 
        public function getRows(
            I\DataRowBuilder $row_builder,
            array $visible_column_ids,
            Range $range,
            Order $order,
            ?array $filter_data,
            ?array $additional_parameters
        ): \Generator {
            $records = $this->getRecords($range, $order);
            foreach ($records as $idx => $record) {
                $row_id = (string)$record['usr_id'];
                $record['achieve_txt'] = $record['achieve'] > 80 ? 'passed' : 'failed';
                $record['failure_txt'] = "not " . $record["achieve_txt"];
                $record['repeat'] = $record['achieve'] < 80;
 
                $icons = [
                    $this->ui_factory->symbol()->icon()->custom('templates/default/images/standard/icon_checked.svg', '', 'small'),
                    $this->ui_factory->symbol()->icon()->custom('templates/default/images/standard/icon_unchecked.svg', '', 'small'),
                    $this->ui_factory->symbol()->icon()->custom('templates/default/images/standard/icon_x.svg', '', 'small'),
                ];
                $icon = $icons[2];
                if($record['achieve'] > 80) {
                    $icon = $icons[0];
                }
                if($record['achieve'] < 30) {
                    $icon = $icons[1];
                }
                $record['achieve'] = $icon;
 
                yield $row_builder->buildDataRow($row_id, $record)
                    /** Actions may be disabled for specific rows: */
                    ->withDisabledAction('delete', ($record['login'] === 'superuser'));
            }
        }
 
        public function getTotalRowCount(
            ?array $filter_data,
            ?array $additional_parameters
        ): ?int {
            return count($this->getRecords());
        }
 
        protected function getRecords(Range $range = null, Order $order = null): array
        {
            $records = [
                ['usr_id' => 123,'login' => 'superuser','email' => 'user@example.com',
                 'last' => (new \DateTimeImmutable())->modify('-1 day') ,'achieve' => 20,'fee' => 0
                ],
                ['usr_id' => 867,'login' => 'student1','email' => 'student1@example.com',
                 'last' => (new \DateTimeImmutable())->modify('-10 day'),'achieve' => 90,'fee' => 40
                ],
                ['usr_id' => 8923,'login' => 'student2','email' => 'student2@example.com',
                 'last' => (new \DateTimeImmutable())->modify('-8 day'),'achieve' => 66,'fee' => 36.789
                ],
                ['usr_id' => 8748,'login' => 'student3_longname','email' => 'student3_long_email@example.com',
                 'last' => (new \DateTimeImmutable())->modify('-300 day'),'achieve' => 8,'fee' => 36.789
                ],
                ['usr_id' => 8749,'login' => 'studentAB','email' => 'studentAB@example.com',
                 'last' => (new \DateTimeImmutable())->modify('-7 day'),'achieve' => 100,'fee' => 114
                ],
                ['usr_id' => 8750,'login' => 'student5','email' => 'student5@example.com',
                 'last' => new \DateTimeImmutable(),'achieve' => 76,'fee' => 3.789
                ],
                ['usr_id' => 8751,'login' => 'student6','email' => 'student6@example.com',
                 'last' => (new \DateTimeImmutable())->modify('-2 day'),'achieve' => 66,'fee' => 67
                ]
            ];
            if ($order) {
                list($order_field, $order_direction) = $order->join([], fn($ret, $key, $value) => [$key, $value]);
                usort($records, fn($a, $b) => $a[$order_field] <=> $b[$order_field]);
                if ($order_direction === 'DESC') {
                    $records = array_reverse($records);
                }
            }
            if ($range) {
                $records = array_slice($records, $range->getStart(), $range->getLength());
            }
 
            return $records;
        }
    };
 
 
    /**
     * setup the Table and hand over the request;
     * with an ID for the table, parameters will be stored throughout url changes
     */
    $table = $f->table()
            ->data('a data table', $columns, $data_retrieval)
            ->withId('example_base')
            ->withActions($actions)
            ->withRequest($request);
 
    /**
     * build some output.
     */
    $out = [$table];
 
    /**
     * get the desired action from query; the parameter is namespaced,
     * but we still have the token and it knows its name:
     */
    $query = $DIC->http()->wrapper()->query();
    if ($query->has($action_parameter_token->getName())) {
        $action = $query->retrieve($action_parameter_token->getName(), $refinery->to()->string());
        /** also get the row-ids and build some listing */
        $ids = $query->retrieve($row_id_token->getName(), $refinery->custom()->transformation(fn($v) => $v));
        $listing = $f->listing()->characteristicValue()->text([
            'table_action' => $action,
            'id' => print_r($ids, true),
        ]);
 
        /** take care of the async-call; 'delete'-action asks for it. */
        if ($action === 'delete') {
            $items = [];
            foreach ($ids as $id) {
                $items[] = $f->modal()->interruptiveItem()->keyValue($id, $row_id_token->getName(), $id);
            }
            echo($r->renderAsync([
                $f->modal()->interruptive(
                    'Deletion',
                    'You are about to delete items!',
                    '#'
                )->withAffectedItems($items)
                ->withAdditionalOnLoadCode(static fn($id): string => "console.log('ASYNC JS');")
            ]));
            exit();
        }
        if ($action === 'info') {
            echo(
                $r->render($f->messageBox()->info('an info message: <br><li>' . implode('<li>', $ids)))
                . '<script data-replace-marker="script">console.log("ASYNC JS, too");</script>'
            );
            exit();
        }
 
        /** otherwise, we want the table and the results below */
        $out[] = $f->divider()->horizontal();
        $out[] = $listing;
    }
 
    return $r->render($out);
}
 

Example 2: Repo implementation

a data table from a repository

User ID
success
repeat
sql order part
sql range part
User ID: 123 Login: superuser last login: Sunday, 05.05.2024 progress: 20 success: failed achieved: repeat: yes Fee: £ 0,00 sql order part: ORDER BY login ASC sql range part: LIMIT 800 OFFSET 0
User ID: 867 Login: student1 last login: Sunday, 05.05.2024 progress: 90 success: passed achieved: repeat: no Fee: £ 40,00 sql order part: ORDER BY login ASC sql range part: LIMIT 800 OFFSET 0
User ID: 8923 Login: student2 last login: Sunday, 05.05.2024 progress: 66 success: failed achieved: repeat: yes Fee: £ 36,79 sql order part: ORDER BY login ASC sql range part: LIMIT 800 OFFSET 0
User ID: 8748 Login: student3_longname last login: Sunday, 05.05.2024 progress: 66 success: failed achieved: repeat: yes Fee: £ 36,79 sql order part: ORDER BY login ASC sql range part: LIMIT 800 OFFSET 0
<?php
 
declare(strict_types=1);
 
namespace ILIAS\UI\examples\Table\Data;
 
use ILIAS\UI\Implementation\Component\Table as T;
use ILIAS\UI\Component\Table as I;
use ILIAS\Data\Range;
use ILIAS\Data\Order;
 
function repo_implementation()
{
    /**
     * A Table is prone to reflect database tables, or, better repository entries.
     * Usually, changes in the available data and their representation go along
     * with each other, so it might be a good idea to keep that together.
     *
     * Here is an example, in which the DataRetrieval extends the repository in
     * which the UI-code becomes _very_ small for the actual representation.
     *
     * Please note that the pagination is missing due to an amount of records
     * smaller than the lowest option "number of rows".
    */
 
    global $DIC;
    $r = $DIC['ui.renderer'];
 
    $repo = new DataTableDemoRepo();
    $table = $repo->getTableForRepresentation();
 
    return $r->render(
        $table->withRequest($DIC->http()->request())
    );
}
 
class DataTableDemoRepo implements I\DataRetrieval
{
    protected \ILIAS\UI\Factory $ui_factory;
    protected \ILIAS\Data\Factory $df;
 
    public function __construct()
    {
        global $DIC;
        $this->ui_factory = $DIC['ui.factory'];
        $this->df = new \ILIAS\Data\Factory();
    }
 
    //the repo is capable of building its table-view (similar to forms from a repo)
    public function getTableForRepresentation(): \ILIAS\UI\Implementation\Component\Table\Data
    {
        return $this->ui_factory->table()->data(
            'a data table from a repository',
            $this->getColumsForRepresentation(),
            $this
        );
    }
 
    //implementation of DataRetrieval - accept params and yield rows
    public function getRows(
        I\DataRowBuilder $row_builder,
        array $visible_column_ids,
        Range $range,
        Order $order,
        ?array $filter_data,
        ?array $additional_parameters
    ): \Generator {
        $icons = [
            $this->ui_factory->symbol()->icon()->custom('templates/default/images/standard/icon_checked.svg', '', 'small'),
            $this->ui_factory->symbol()->icon()->custom('templates/default/images/standard/icon_unchecked.svg', '', 'small')
        ];
        foreach ($this->doSelect($order, $range) as $idx => $record) {
            $row_id = (string)$record['usr_id'];
            $record['achieve_txt'] = $record['achieve'] > 80 ? 'passed' : 'failed';
            $record['failure_txt'] = "not " . $record["achieve_txt"];
            $record['repeat'] = $record['achieve'] < 80;
            $record['achieve_icon'] = $icons[(int) $record['achieve'] > 80];
            yield $row_builder->buildDataRow($row_id, $record);
        }
    }
 
    public function getTotalRowCount(
        ?array $filter_data,
        ?array $additional_parameters
    ): ?int {
        return count($this->dummyrecords());
    }
 
    //do the actual reading - note, that e.g. order and range are easily converted to SQL
    protected function doSelect(Order $order, Range $range): array
    {
        $sql_order_part = $order->join('ORDER BY', fn(...$o) => implode(' ', $o));
        $sql_range_part = sprintf('LIMIT %2$s OFFSET %1$s', ...$range->unpack());
        return array_map(
            fn($rec) => array_merge($rec, ['sql_order' => $sql_order_part, 'sql_range' => $sql_range_part]),
            $this->dummyrecords()
        );
    }
 
    //this is how the UI-Table looks - and that's usually quite close to the db-table
    protected function getColumsForRepresentation(): array
    {
        $f = $this->ui_factory;
        return  [
            'usr_id' => $f->table()->column()->number("User ID")
                ->withIsSortable(false),
            'login' => $f->table()->column()->text("Login")
                ->withHighlight(true),
            'email' => $f->table()->column()->eMail("eMail"),
            'last' => $f->table()->column()->date("last login", $this->df->dateFormat()->germanLong()),
            'achieve' => $f->table()->column()->status("progress")
                ->withIsOptional(true),
            'achieve_txt' => $f->table()->column()->status("success")
                ->withIsSortable(false)
                ->withIsOptional(true),
            'failure_txt' => $f->table()->column()->status("failure")
                ->withIsSortable(false)
                ->withIsOptional(true, false),
            'achieve_icon' => $f->table()->column()->statusIcon("achieved")
                ->withIsOptional(true),
            'repeat' => $f->table()->column()->boolean("repeat", 'yes', 'no')
                ->withIsSortable(false),
            'fee' => $f->table()->column()->number("Fee")
                ->withDecimals(2)
                ->withUnit('£', I\Column\Number::UNIT_POSITION_FORE),
            'sql_order' => $f->table()->column()->text("sql order part")
                ->withIsSortable(false)
                ->withIsOptional(true),
            'sql_range' => $f->table()->column()->text("sql range part")
                ->withIsSortable(false)
                ->withIsOptional(true)
        ];
    }
 
    protected function dummyrecords()
    {
        return [
            ['usr_id' => 123,'login' => 'superuser','email' => 'user@example.com',
             'last' => new \DateTimeImmutable(),'achieve' => 20,'fee' => 0
            ],
            ['usr_id' => 867,'login' => 'student1','email' => 'student1@example.com',
             'last' => new \DateTimeImmutable(),'achieve' => 90,'fee' => 40
            ],
            ['usr_id' => 8923,'login' => 'student2','email' => 'student2@example.com',
             'last' => new \DateTimeImmutable(),'achieve' => 66,'fee' => 36.789
            ],
            ['usr_id' => 8748,'login' => 'student3_longname','email' => 'student3_long_email@example.com',
             'last' => new \DateTimeImmutable(),'achieve' => 66,'fee' => 36.789
            ]
        ];
    }
}
 

Example 3: Without data

Empty Data Table

Column 1
Column 2
: No records
<?php
 
declare(strict_types=1);
 
namespace ILIAS\UI\examples\Table\Data;
 
use ILIAS\UI\Component\Table\DataRetrieval;
use ILIAS\UI\Component\Table\DataRowBuilder;
use ILIAS\Data\Range;
use ILIAS\Data\Order;
use Generator;
 
/**
 * Example showing a data table without any data and hence no entries, which
 * will automatically display an according message.
 */
function without_data(): string
{
    global $DIC;
 
    $factory = $DIC->ui()->factory();
    $renderer = $DIC->ui()->renderer();
    $request = $DIC->http()->request();
 
    $empty_retrieval = new class () implements DataRetrieval {
        public function getRows(
            DataRowBuilder $row_builder,
            array $visible_column_ids,
            Range $range,
            Order $order,
            ?array $filter_data,
            ?array $additional_parameters
        ): Generator {
            yield from [];
        }
 
        public function getTotalRowCount(?array $filter_data, ?array $additional_parameters): ?int
        {
            return 0;
        }
    };
 
    $table = $factory->table()->data(
        'Empty Data Table',
        [
            'col1' => $factory->table()->column()->text('Column 1')
                ->withIsSortable(false),
            'col2' => $factory->table()->column()->number('Column 2')
                ->withIsSortable(false),
        ],
        $empty_retrieval
    );
 
    return $renderer->render($table->withRequest($request));
}
 

Relations

Parents
  1. UIComponent
  2. Table