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
- Tables MUST NOT be used to merely arrange elements visually; displayed records MUST have a certain consistency of content.
- A Data Table SHOULD have at least 3 Columns.
- A Data Table SHOULD potentially have an unlimited number of rows.
- Rows in the table MUST be of the same structure.
- Tables MUST NOT have more than one View Control of a kind, e.g. a second pagination would be forbidden.
- Interaction
- View Controls used here MUST only affect the table itself.
- Accessibility
- The HTML tag enclosing the actual tabular presentation MUST have the role-attribute "grid".
- 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!)
- 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".
- The HTML tag enclosing one record MUST have the role-attribute "row".
- A single cell MUST be marked with the role-attribute "gridcell".
- 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 | eMail: student1@example.com | last login: 12.12.2024 18:50 | progress: | success: passed | repeat: no | Fee: £ 40,00 |
|
|
User ID: 8923 | Login: student2 | eMail: student2@example.com | last login: 14.12.2024 18:50 | progress: | success: failed | repeat: yes | Fee: £ 36,79 |
|
|
User ID: 8748 | Login: student3_longname | eMail: student3_long_email@example.com | last login: 26.02.2024 18:50 | progress: | success: failed | repeat: yes | Fee: £ 36,79 |
|
|
User ID: 8750 | Login: student5 | eMail: student5@example.com | last login: 22.12.2024 18:50 | progress: | success: failed | repeat: yes | Fee: £ 3,79 |
|
|
User ID: 8751 | Login: student6 | eMail: student6@example.com | last login: 20.12.2024 18:50 | progress: | success: failed | repeat: yes | Fee: £ 67,00 |
|
|
User ID: 8749 | Login: studentAB | eMail: studentAB@example.com | last login: 15.12.2024 18:50 | progress: | success: passed | repeat: no | Fee: £ 114,00 |
|
|
User ID: 123 | Login: superuser | eMail: user@example.com | last login: 21.12.2024 18:50 | 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(); $current_user_date_format = $df->dateFormat()->withTime24( $DIC['ilUser']->getDateFormat() ); /** * 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", $current_user_date_format), '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 | eMail: user@example.com | last login: 22.12.2024 18:50 | progress: 20 | success: failed | achieved: | repeat: yes | Fee: £ 0,00 | sql order part: ORDER BY login ASC | sql range part: LIMIT 25 OFFSET 0 |
User ID: 867 | Login: student1 | eMail: student1@example.com | last login: 22.12.2024 18:50 | progress: 90 | success: passed | achieved: | repeat: no | Fee: £ 40,00 | sql order part: ORDER BY login ASC | sql range part: LIMIT 25 OFFSET 0 |
User ID: 8923 | Login: student2 | eMail: student2@example.com | last login: 22.12.2024 18:50 | progress: 66 | success: failed | achieved: | repeat: yes | Fee: £ 36,79 | sql order part: ORDER BY login ASC | sql range part: LIMIT 25 OFFSET 0 |
User ID: 8748 | Login: student3_longname | eMail: student3_long_email@example.com | last login: 22.12.2024 18:50 | progress: 66 | success: failed | achieved: | repeat: yes | Fee: £ 36,79 | sql order part: ORDER BY login ASC | sql range part: LIMIT 25 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; protected \ILIAS\Data\DateFormat\DateFormat $current_user_date_format; public function __construct() { global $DIC; $this->ui_factory = $DIC['ui.factory']; $this->df = new \ILIAS\Data\Factory(); $this->current_user_date_format = $this->df->dateFormat()->withTime24( $DIC['ilUser']->getDateFormat() ); } //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->current_user_date_format), '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
- UIComponent
- Table