@financial-times/o-table
Version:
Provides styling and behvaiour for tables across FT products.
604 lines (487 loc) • 21.3 kB
Markdown
# o-table
Styling for tables.
- [o-table](#o-table)
- [Usage](#usage)
- [Markup](#markup)
- [Basic table](#basic-table)
- [Sort Order](#sort-order)
- [Disable sort](#disable-sort)
- [Responsive options](#responsive-options)
- [Expander](#expander)
- [Additional markup](#additional-markup)
- [Sass](#sass)
- [JavaScript](#javascript)
- [Filtering](#filtering)
- [Filter (declarative)](#filter-declarative)
- [Filter (imperative)](#filter-imperative)
- [Sorting](#sorting)
- [Custom sort (declarative)](#custom-sort-declarative)
- [Custom sort (imperative)](#custom-sort-imperative)
- [Dynamic Rows](#dynamic-rows)
- [Events](#events)
- [oTable.ready](#otableready)
- [oTable.sorted](#otablesorted)
- [oTable.sorting](#otablesorting)
- [Get The Sorted Column Heading From A Sort Event](#get-the-sorted-column-heading-from-a-sort-event)
- [Migration](#migration)
- [Contact](#contact)
- [Licence](#licence)
## Usage
Check out [how to include Origami components in your project](https://origami.ft.com/documentation/components/#including-origami-components-in-your-project) to get started with `o-table`.
## Markup
### Basic table
Add an `o-table` class to any table you wish to apply the styles to:
```html
<table class="o-table" data-o-component="o-table">
...
</table>
```
Where table headings (`th`) are used as row headings, `scope="row"` attributes must be set on the `th`:
```html
<table class="o-table" data-o-component="o-table">
<tbody>
<tr>
<th scope="row" role="rowheader">Item</th>
<td>Holiday</td>
<td>Lunch</td>
</tr>
<tr>
<th scope="row" role="rowheader">Cost</th>
<td>£123.45</td>
<td>£7</td>
</tr>
</tbody>
...
</table>
```
The table's `caption` element should include a header of the appropriate level and style for the table's context.
```html
<table class="o-table" data-o-component="o-table">
<caption class="o-table__caption">
<h2>My Table Caption</h2>
</caption>
<thead>
...
</thead>
<tbody>
...
</tbody>
...
</table>
```
The table's footer `tfoot` element may use the helper class `o-table-footnote` to display sources, disclaimers, etc.
```html
<table class="o-table" data-o-component="o-table">
<thead>
...
</thead>
<tbody>
...
</tbody>
<tfoot>
<tr>
<td colspan="2" class="o-table-footnote">Source: The Origami team.</td>
</tr>
</tfoot>
</table>
```
### Sort Order
When a sortable table column is clicked an ascending sort is applied by default. If clicked again the sort order is toggled to a descending sort. Set the preferred sort order attribute `data-o-table-preferred-sort-order="descending"` to inverse this, so a descending sort is applied on the first click.
```html
<table
class="o-table"
data-o-component="o-table"
data-o-table-preferred-sort-order="descending"
></table>
```
### Disable sort
Table columns are sortable by default but may be disabled by adding `data-o-table-sortable="false"` to the table.
```html
<table
class="o-table"
data-o-component="o-table"
data-o-table-sortable="false"
></table>
```
Or to disable sort per table column, add `data-o-table-heading-disable-sort` to the column's `th` element.
```html
<table class="o-table" data-o-component="o-table">
<thead>
<tr>
<th>Heading One</th>
<!-- do not show the actions column as sortable -->
<th data-o-table-heading-disable-sort>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>Item One</td>
<td><a href="#edit">edit</a></td>
</tr>
</tbody>
</table>
```
### Responsive options
There are three options for small viewports where the table does not fit.
1. [overflow](https://registry.origami.ft.com/components/o-table#demo-responsive-overflow) - Scroll the whole table including headings horizontally. This option also supports an [expander](#expander).
2. [scroll](https://registry.origami.ft.com/components/o-table#demo-responsive-scroll) - Flip the table so headings are in the first column and sticky, data is scrollable horizontally.
3. [flat](https://registry.origami.ft.com/components/o-table#demo-responsive-flat) - Split each row into an individual item and repeat headings.
To enable these set `data-o-table-responsive` to the type of responsive table desired and add the classes for that type of table. Then wrap the table in `o-table-container`, `o-table-overlay-wrapper`, `o-table-scroll-wrapper`. E.g for an "overflow" table:
```html
<div class="o-table-container">
<div class="o-table-overlay-wrapper">
<div class="o-table-scroll-wrapper">
<table
class="o-table o-table--horizontal-lines o-table--responsive-overflow"
data-o-component="o-table"
data-o-table-responsive="overflow"
>
...
</table>
</div>
</div>
</div>
```
If your project does not use the build service, you may need to specify an [extra Sass option](#sass) for responsive features and initialise [o-table JavaScript](#JavaScript).
More examples are available in [the registry](https://registry.origami.ft.com/components/o-table).
### Expander
The "overflow" style of responsive table ([see above](#responsive-options)) supports an expander to hide rows and offer a "show more" / "show fewer" button. To enable this feature set `data-o-table-expanded="false"` to the table. The number of rows to show when the table is not expanded can be configured with `data-o-table-minimum-row-count="20"` _(default: 20)_.
```html
<div class="o-table-container">
<div class="o-table-overlay-wrapper">
<div class="o-table-scroll--wrapper">
<table
class="o-table o-table--horizontal-lines o-table--responsive-overflow"
data-o-component="o-table"
data-o-table-responsive="overflow"
data-o-table-expanded="false"
data-o-table-minimum-row-count="10"
>
...
</table>
</div>
</div>
</div>
```
To add a footnote to an expandable table, for example with disclaimers or sources, add the footnote within the container and link to the table with an id and the `aria-describedby` attribute. If not working on an expandable table, [use the `tfoot` element instead](#basic-table).
```diff
<div class="o-table-container">
<div class="o-table-overlay-wrapper">
<div class="o-table-scroll--wrapper">
+ <table aria-describedby="demo-footnote">
...
</table>
</div>
</div>
+ <div id="demo-footnode" class="o-table-footnote">
+ Source: The Origami team's love of fruit.
+ </div>
</div>
```
### Additional markup
- `o-table--compact` - Apply to the table for smaller typography and padding.
- `o-table--row-stripes` - Apply to the table for alternating stripes on the table rows.
- `o-table-footnote` - Style a `tfoot` element subtily for sources, disclaimers, etc.
- `o-table__cell--numeric` - Apply to numeric cells to align content to the right.
- `o-table__cell--vertically-center` - Apply to cells which should center vertically.
See more in the registry: [o-table demos](https://registry.origami.ft.com/components/o-table).
## Sass
Use `@include oTable()` to include styles for all table features. Alternatively styles may be included granularly with an `$opts` map.
Include all table features:
```scss
@include oTable();
```
Alternatively include base styles with only selected optional features. E.g. to include only the "overflow" responsive table and styles for table lines:
```scss
@include oTable(
$opts: (
'responsive-overflow',
'lines',
)
);
```
| Feature | Description | Brand support |
| ------------------- | ------------------------------------------------------- | -------------------------- |
| responsive-overflow | See [responsive options](#responsive-options). | core, internal, whitelabel |
| responsive-flat | See [responsive options](#responsive-options). | core, internal, whitelabel |
| responsive-scroll | See [responsive options](#responsive-options). | core, internal, whitelabel |
| lines | Styles for horizontal and vertical lines, plus borders. | core, internal, whitelabel |
| compact | A table with smaller typography and padding. | core, internal, whitelabel |
| stripes | Alternating row stripe styles. | core, internal |
| row-headings | Row heading styles. | internal |
## JavaScript
To manually instantiate `o-table`:
```js
import OTable from '@financial-times/o-table';
OTable.init();
```
or
```js
import OTable from '@financial-times/o-table';
oTable = new OTable(document.body);
```
This will return an instance of `BasicTable` (default), `OverflowTable`, `FlatTable`, or `ScrollTable` depending on the value of `data-o-table-responsive`. All four table types extend `BaseTable`.
Instantiation will add column sorting to all tables. It will also add scroll controls and, if configured, an [expander](#expander) to any `OverflowTable`. These can be configured with [data attributes](#disable-sort) or imperatively with an options object:
```js
import OTable from '@financial-times/o-table';
OTable.init(document.body, {
sortable: true,
expanded: true,
preferredSortOrder: 'ascending',
minimumRowCount: 10,
});
```
### Filtering
All `o-table` instances support filtering on a single column. Filters may be applied declaratively in HTML or by calling the `o-table` JavaScript method `filter`.
The style of form elements used to filter a table are not determined by `o-table`. However we recommend using [o-forms](https://registry.origami.ft.com/components/o-forms) to style form elements used to filter an `o-table`, such as `input` or `select` elements. See the [o-table filter demos](https://registry.origami.ft.com/components/o-table#demo-filter) in the component registry for a demo using `o-forms` styles.
#### Filter (declarative)
Declarative filters are case insensitive and perform partial matches, e.g. a filter of "Kingdom" would reveal "United Kingdom".
To enable declarative table filtering add the `data-o-table-filter-id` and `data-o-table-filter-column` to a form input. Where `data-o-table-filter-id` matches the `id` of the table to filter and `data-o-table-filter-column` is the numerical index of the column to filter (starting at 0).
For example, to filter a table based on a users selected option:
```html
<label>Filter the table by country:</label>
<!-- the filter input specifies the table id in "data-o-table-filter-id" -->
<select data-o-table-filter-id="example-table" data-o-table-filter-column="0">
<option value="" selected>All</option>
<option value="Austria">Austria</option>
<option value="Belgium">Belgium</option>
<!-- more options -->
</select>
<!-- the table markup, this may be a responsive table -->
<div class="o-table-container">
<!-- the table element with an id -->
<table id="example-table">
<!-- ... -->
</table>
</div>
```
Or to filter a table based on a users selected option:
```html
<label>Filter the table by country:</label>
<!-- the filter input specifies the table id in "data-o-table-filter-id" -->
<input
type="text"
data-o-table-filter-id="example-table"
data-o-table-filter-column="0"
/>
<!-- the table markup, this may be a responsive table -->
<div class="o-table-container">
<!-- the table element with an id -->
<table id="example-table">
<!-- ... -->
</table>
</div>
```
#### Filter (imperative)
The table's `filter` method may also be used to filter the table. Call it with the column index to filter and the filter to apply. The filter may be a string, which acts like a declarative filter (i.e. is case insensitive and performs a partial match):
```js
const table = new OTable(tableElement);
table.filter(0, 'United Kingdom'); // Filter the first table column by "United Kingdom".
```
Alternatively a callback function may be given. The callback should accept a table cell element and return a boolean value:
```js
const table = new OTable(tableElement);
table.filter(0, cell => {
return parseInt(cell.textContent, 10) > 3;
}); // Filter the first table column. Keep rows with a value more than 3.
```
### Sorting
All `o-table` instances support sorting. Sorting on non-string values such as numbers works if the column type has been declared. E.g. for a column of numbers add the following to `o-table`:
`data-o-table-data-type="number"`.
Other data types for `data-o-table-data-type` include:
| type | description | examples |
| -------- | --------------------------------------------------------------------------------------------- | ------------------------------------------ |
| text | The default, content is sorted as a string. | "Jane Doe", "John Smith" |
| date | Supports the FT style of date and time, including abbreviation. | "Aug 17", "1.30am", "April 20 2014 1.30pm" |
| number | Any number which may include number formatting and abbreviation. | "1,200", "100", "3.2", "1bn", "2tn" |
| percent | Any percentage with or without the symbol "%". | "3.3%", "200%", "50%" |
| currency | Any currency, which may include number formatting, number abbreviation, and currency symbols. | "$84m", "£36bn", "HK$90bn", "Rp14.595" |
| numeric | A column which may be treated as numeric which does not fit a more specific type. | "101 dalmatians" |
It is possible to add sort support for a custom data type. Alternatively, the behaviour of an existing type may be modified.
#### Custom sort (declarative)
If you are wanting to sort by a custom pattern, you can apply the sorting values to each row as a data attribute:
`data-o-table-sort-value=${sort-value}`. These values can be strings or integers.
For example to support a custom date format set `data-o-table-sort-value` to its UNIX Epoch.
```html
<table class="o-table" data-o-component="o-table">
<thead>
<tr>
<th data-o-table-data-type="date">Custom Date Formats</th>
</tr>
</thead>
<tbody>
<tr>
<td data-o-table-sort-value="1533081600">Wednesday, 1 August 2018</td>
</tr>
<tr>
<td data-o-table-sort-value="1483228800">Jan 2017</td>
</tr>
<tr>
<td data-o-table-sort-value="723168000">1st December 1992</td>
</tr>
</tbody>
</table>
```
Or to provide an arbitrary sort order:
```html
<table class="o-table" data-o-component="o-table">
<thead>
<tr>
<th>Things</th>
</tr>
</thead>
<tbody>
<tr>
<td data-o-table-sort-value="2">snowman</td>
</tr>
<tr>
<td data-o-table-sort-value="3">42</td>
</tr>
<tr>
<td data-o-table-sort-value="1">pangea</td>
</tr>
</tbody>
</table>
```
#### Custom sort (imperative)
Rather than specify `data-o-table-sort-value` [declaratively](#custom-sort-declarative), a formatter function may be provided client-side to generate sort values for a given data type.
For example we could add support for a custom data type `emoji-time`.
```html
<table class="o-table" data-o-component="o-table">
<thead>
<tr>
<th data-o-table-data-type="emoji-time">Emoji Time</th>
</tr>
</thead>
<tbody>
<tr>
<td>🌑</td>
</tr>
<tr>
<td>🌤️️</td>
</tr>
<tr>
<td>🌑</td>
</tr>
<tr>
<td>🌤️️</td>
</tr>
</tbody>
</table>
```
To do that call `setSortFormatterForType` with the custom data type and a formatter function.
The formatter accepts the table cell (HTMLElement) and returns a sort value (Number or String) for that cell.
In this case we add support for our custom type `emoji-time` by assigning the emoji a numerical sort value. This will effect all tables instantiated by `OTable`.
```js
import OTable from '@financial-times/o-table';
// Set a filter for custom data type "emoji-time".
// The return value may be a string or number.
OTable.setSortFormatterForType('emoji-time', cell => {
const text = cell.textContent.trim();
if (text === '🌑') {
return 1;
}
if (text === '🌤️️') {
return 2;
}
return 0;
});
OTable.init();
```
Which for an ascending sort, will result in:
```html
<table class="o-table" data-o-component="o-table">
<thead>
<tr>
<th data-o-table-data-type="emoji-time" aria-sort="ascending">
Emoji Time
</th>
</tr>
</thead>
<tbody>
<tr>
<td data-o-table-sort-value="1">🌑</td>
</tr>
<tr>
<td data-o-table-sort-value="1">🌑</td>
</tr>
<tr>
<td data-o-table-sort-value="2">🌤️️</td>
</tr>
<tr>
<td data-o-table-sort-value="2">🌤️️</td>
</tr>
</tbody>
</table>
```
### Dynamic Rows
If rows are added or removed dynamically after the table is initialised call `updateRows`; this will apply any existing sort or filter to the new rows.
### Events
The following events are fired by `o-table`.
- `oTable.ready`
- `oTable.sorting`
- `oTable.sorted`
#### oTable.ready
`oTable.ready` fires when the table has been initialised.
The event provides the following properties:
- `detail.instance` - The initialised `o-table` instance _(FlatTable | ScrollTable | OverflowTable | BasicTable)_.
#### oTable.sorted
`oTable.sorted` indicates a table has finished sorting. It includes details of the current sort status of the table.
The event provides the following properties:
- `detail.sortOrder` - The sort order e.g. "ascending" _(String)_.
- `detail.columnIndex` - The index of the sorted column heading _(Number)_.
- `detail.instance` - The effected `o-table` instance _(FlatTable | ScrollTable | OverflowTable | BasicTable)_.
```js
document.addEventListener('oTable.sorted', event => {
console.log(
`The target table was just sorted by column "${event.detail.columnIndex}" in an "${event.detail.sortOrder}" order.`
);
});
```
#### oTable.sorting
This event is fired just before a table sorts based on user interaction. It may be prevented to implement custom sort functionality. This may be useful to sort a paginated table server-side.
The event provides the following properties:
- `detail.sort` - The sort requested e.g. "ascending" _(String)_.
- `detail.columnIndex` - The index of the column heading which will be sorted _(Number)_.
- `detail.instance` - The effected `o-table` instance _(FlatTable | ScrollTable | OverflowTable | BasicTable)_.
When intercepting the default sort the `sorted` method must be called with relevant parameters when the custom sort is completed.
```js
document.addEventListener('oTable.sorting', event => {
// Prevent default sorting.
event.preventDefault();
// Update the table with a custom sort.
console.log(`Update the table with sorted data here.`);
// Fire the sorted event, passing along the column index and sort.
event.detail.instance.sorted({
columnIndex: event.detail.columnIndex,
sortOrder: event.detail.sort,
});
});
```
#### Get The Sorted Column Heading From A Sort Event
`o-table` sort events provide a `columnIndex`. This index maps to a column heading. To retrieve the column heading use `getTableHeader`.
```js
document.addEventListener('oTable.sorting', event => {
const table = event.detail.instance;
const columnIndex = event.detail.columnIndex;
// Get the table header from the column index.
console.log(table.getTableHeader(columnIndex));
});
```
## Migration
| State | Major Version | Last Minor Release | Migration guide |
| :----------: | :-----------: | :----------------: | :-----------------------------------------------------: |
| ⚠ maintained | 10 | N/A | [migrate to v10](MIGRATION.md#migrating-from-v9-to-v10) |
| ╳ deprecated | 9 | 9.3 | [migrate to v9](MIGRATION.md#migrating-from-v8-to-v9) |
| ╳ deprecated | 8 | 8.1 | [migrate to v8](MIGRATION.md#migrating-from-v7-to-v8) |
| ╳ deprecated | 7 | 7.4 | [migrate to v7](MIGRATION.md#migrating-from-v6-to-v7) |
| ╳ deprecated | 6 | 6.9 | [migrate to v6](MIGRATION.md#migrating-from-v5-to-v6) |
| ╳ deprecated | 5 | 5.2 | [migrate to v5](MIGRATION.md#migrating-from-v4-to-v5) |
| ╳ deprecated | 4 | 4.1 | [migrate to v4](MIGRATION.md#migrating-from-v3-to-v4) |
| ╳ deprecated | 3 | 3.0 | - |
| ╳ deprecated | 2 | 2.0 | - |
| ╳ deprecated | 1 | 1.7 | - |
## Contact
If you have any questions or comments about this component, or need help using it, please either [raise an issue](https://github.com/Financial-Times/o-table/issues), visit [#origami-support](https://financialtimes.slack.com/messages/origami-support/) or email [Origami Support](mailto:origami-support@ft.com).
## Licence
This software is published by the Financial Times under the [MIT licence](http://opensource.org/licenses/MIT).