npm package: @financial-times/o-table
Description: Provides styling and behvaiour for tables across FT products.
Latest version: 10.0.2

# 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).