# DataTable

A table for presenting structured, scannable tabular data. Also known as table, data grid, or grid.

## Installation

Install the component, the theme package, and **the font package matching your
project's brand**:

**Leroy Merlin projects**

```bash
npm install @ptlm-azulejo/data-table @ptlm-azulejo/themes @ptlm-azulejo/fonts-leroy-merlin
# or
yarn add @ptlm-azulejo/data-table @ptlm-azulejo/themes @ptlm-azulejo/fonts-leroy-merlin
```

**Adeo projects**

```bash
npm install @ptlm-azulejo/data-table @ptlm-azulejo/themes @ptlm-azulejo/fonts-adeo
# or
yarn add @ptlm-azulejo/data-table @ptlm-azulejo/themes @ptlm-azulejo/fonts-adeo
```

`@ptlm-azulejo/pagination` and `@ptlm-azulejo/icons` come along as dependencies —
the table imports their stylesheets itself, so there is nothing extra to wire up.

## Styles & theming

The component ships no colors or typeface of its own — it reads design tokens from
CSS variables at runtime. Those tokens come from `@ptlm-azulejo/themes`, and **the
brand is selected by a class on your app's `<html>` element**, so switching brand
never touches component code.

| Project      | Preset stylesheet                               | Root class    | Typeface        | Font package                       |
| ------------ | ----------------------------------------------- | ------------- | --------------- | ---------------------------------- |
| Leroy Merlin | `@ptlm-azulejo/themes/presets/leroy-merlin.css` | `preset-lm`   | LeroyMerlinSans | `@ptlm-azulejo/fonts-leroy-merlin` |
| Adeo         | `@ptlm-azulejo/themes/presets/adeo.css`         | `preset-adeo` | Roboto          | `@ptlm-azulejo/fonts-adeo`         |

**Leroy Merlin projects**

```js
import '@ptlm-azulejo/themes/presets/leroy-merlin.css'
import '@ptlm-azulejo/fonts-leroy-merlin'
import '@ptlm-azulejo/data-table/style.css'
```

```html
<html lang="pt" class="preset-lm"></html>
```

**Adeo projects**

```js
import '@ptlm-azulejo/themes/presets/adeo.css'
import '@ptlm-azulejo/fonts-adeo'
import '@ptlm-azulejo/data-table/style.css'
```

```html
<html lang="pt" class="preset-adeo"></html>
```

> The preset class is what resolves the brand at runtime. Without it — even with
> the stylesheets imported — the component renders uncolored and in a fallback
> typeface. See the [themes package](../themes/README.md) for brand switching,
> dark mode, and custom brands.

### Light and dark mode

Add `data-theme` alongside the brand class to pin the color scheme. Leave it off
and the preset follows the OS `prefers-color-scheme`:

```html
<html lang="pt" class="preset-lm" data-theme="dark"></html>
```

### Why the font package is separate

The preset only _names_ its typeface in `--font-family` and ships no font files.
[Loading them is your app's job](../themes/README.md#fonts), as with upstream
Mozaic, so you keep control of hosting, subsetting and preload. Without the
matching font package, `font-sans` falls back to a generic sans-serif. A
multi-brand app can install both and switch by swapping the `.preset-*` class:
only the active brand's file is ever downloaded.

## Props

| Name                    | Type                          | Default                                        | Description                                                                            |
| ----------------------- | ----------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
| `value`                 | `DataTableRow[] \| null`      | `[]`                                           | Rows to render                                                                         |
| `dataKey`               | `string \| ((row) => string)` | —                                              | Dot-path or function used as each row's identity. Falls back to the row index          |
| `caption`               | `string`                      | —                                              | Accessible table caption                                                               |
| `loading`               | `boolean`                     | `false`                                        | Shows a loading overlay over the table                                                 |
| `emptyMessage`          | `string`                      | `'No data to display'`                         | Empty-state title. Override for localisation                                           |
| `emptyDescription`      | `string`                      | —                                              | Optional empty-state description under the title                                       |
| `emptyActionLabel`      | `string`                      | —                                              | Label of the built-in empty-state action. Omit to hide the button                      |
| `emptyActionIcon`       | `string`                      | —                                              | Azulejo icon class for that button (e.g. `az-ui-filter`)                               |
| `loadingMessage`        | `string`                      | `'Loading'`                                    | Loading overlay copy. Override for localisation                                        |
| `sortLabel`             | `string`                      | `'Sort by'`                                    | Prefix of the sort button accessible name                                              |
| `size`                  | `'s' \| 'm'`                  | `'m'`                                          | Cell padding. `m` is the default, `s` is tighter                                       |
| `striped`               | `boolean`                     | `false`                                        | Alternating row background                                                             |
| `gridlines`             | `boolean`                     | `false`                                        | Draws a border around every header and body cell                                       |
| `scrollHeight`          | `string`                      | —                                              | Max height of the table clip region. Toolbar and pager stay put                        |
| `stickyHeader`          | `boolean`                     | `false`                                        | Pins column headers (and the footer row) inside the scroll viewport                    |
| `selectionMode`         | `'single' \| 'multiple'`      | —                                              | Shows a selection column. Keyboard: arrows, Space/Enter, Shift+Arrow range             |
| `selection`             | `DataTableSelection`          | —                                              | Selected row or rows (`v-model:selection`)                                             |
| `selectAllLabel`        | `string`                      | `'Select all'`                                 | Accessible name of the select-all checkbox                                             |
| `selectRowLabel`        | `string`                      | `'Select'`                                     | Accessible name of each row's selection control                                        |
| `sortField`             | `string`                      | —                                              | Sorted column field (`v-model:sort-field`)                                             |
| `sortOrder`             | `1 \| -1 \| null`             | `null`                                         | `1` ascending, `-1` descending (`v-model:sort-order`)                                  |
| `sortMode`              | `'single' \| 'multiple'`      | `'single'`                                     | `multiple` accumulates columns in `multiSortMeta`                                      |
| `multiSortMeta`         | `DataTableSortMeta[]`         | `[]`                                           | Ordered sort keys when `sortMode` is `multiple` (`v-model:multi-sort-meta`)            |
| `removableSort`         | `boolean`                     | `false`                                        | When true, a third click clears the sort. Default keeps the column sorted (asc ↔ desc) |
| `defaultSortOrder`      | `1 \| -1`                     | `1`                                            | Sort direction applied on the first click of an unsorted column                        |
| `nullSortOrder`         | `1 \| -1`                     | `1`                                            | Where null values sit relative to concrete values. `1` after, `-1` before              |
| `lazy`                  | `boolean`                     | `false`                                        | Emit sort and page events without reordering or slicing rows locally                   |
| `paginator`             | `boolean`                     | `false`                                        | Shows AzPagination and slices `value` into pages                                       |
| `first`                 | `number`                      | `0`                                            | 0-based index of the first row on the current page (`v-model:first`)                   |
| `rows`                  | `number`                      | `10`                                           | Rows per page (`v-model:rows`)                                                         |
| `totalRecords`          | `number`                      | —                                              | Total row count for page options. Defaults to `value.length`                           |
| `alwaysShowPaginator`   | `boolean`                     | `true`                                         | When false, the paginator is hidden if there is only one page                          |
| `paginatorPosition`     | `'top' \| 'bottom' \| 'both'` | `'bottom'`                                     | Where to render the paginator                                                          |
| `rowsPerPageOptions`    | `number[]`                    | —                                              | Options for the rows-per-page select. Omit to hide it                                  |
| `rowsPerPageLabel`      | `string`                      | `'Rows per page'`                              | Accessible name of the rows-per-page select                                            |
| `paginatorId`           | `string`                      | —                                              | `id` forwarded to AzPagination. Generated when omitted                                 |
| `paginatorCompact`      | `boolean`                     | `false`                                        | Compact AzPagination: previous/next only                                               |
| `pageMessage`           | `string`                      | `'Page {page} of {pageCount}'`                 | Page option label. Override for localisation                                           |
| `paginatorAriaLabel`    | `string`                      | `'pagination'`                                 | Accessible name of the pagination landmark                                             |
| `paginatorSelectLabel`  | `string`                      | `'Select page'`                                | Accessible name of the page select                                                     |
| `paginatorUi`           | `PaginationUi`                | `{}`                                           | Tailwind class overrides for AzPagination parts                                        |
| `toolbar`               | `boolean`                     | `false`                                        | Shows the built-in Mozaic toolbar above the table                                      |
| `globalFilter`          | `string`                      | —                                              | Search value (`v-model:global-filter`). When set, shows the built-in search            |
| `searchPlaceholder`     | `string`                      | `I'm looking for...`                           | Search field placeholder. Override for localisation                                    |
| `searchLabel`           | `string`                      | `'Search'`                                     | Accessible name of the search field                                                    |
| `filters`               | `DataTableFilterTag[]`        | `[]`                                           | Active filter tags (`{ id, label }`) under the toolbar                                 |
| `selectionCountMessage` | `string`                      | `'{count} selected'`                           | Toolbar selection count. Override for localisation                                     |
| `pageSelectedMessage`   | `string`                      | `'All {count} rows on this page are selected'` | Banner copy when the page is fully selected                                            |
| `selectAllInTableLabel` | `string`                      | `'Select all {count} rows in the table'`       | Banner link to select every row. `{count}` is `totalRecords`                           |
| `clearSelectionLabel`   | `string`                      | `'Clear selection'`                            | Banner link when every row is selected                                                 |
| `resetFiltersLabel`     | `string`                      | `'Reset'`                                      | Filter-tags reset control                                                              |
| `removeFilterLabel`     | `string`                      | `'Remove {label}'`                             | Accessible name of each tag dismiss control                                            |
| `rowClass`              | `(row) => string`             | —                                              | Tailwind classes for a body row, from the row data                                     |
| `expandable`            | `boolean`                     | `false`                                        | Shows an expander column. Pair with `#expansion` and `v-model:expanded-rows`           |
| `expandedRows`          | `DataTableRow[]`              | —                                              | Expanded rows (`v-model:expanded-rows`)                                                |
| `expandLabel`           | `string`                      | `'Expand'`                                     | Accessible name of the expand control                                                  |
| `collapseLabel`         | `string`                      | `'Collapse'`                                   | Accessible name of the collapse control                                                |
| `ui`                    | `DataTableUi`                 | `{}`                                           | Per-part Tailwind class overrides                                                      |

Declare columns with `<AzColumn>` children — there is no `columns` prop.

### AzColumn

| Name          | Type                           | Default  | Description                                                                         |
| ------------- | ------------------------------ | -------- | ----------------------------------------------------------------------------------- |
| `field`       | `string`                       | —        | Dot-path into the row. Also used as the column identity when `columnKey` is omitted |
| `header`      | `string`                       | —        | Visible header label                                                                |
| `columnKey`   | `string`                       | —        | Stable id used as the Vue key and as the `#cell-{key}` slot name                    |
| `sortable`    | `boolean`                      | `false`  | Header is a button that toggles this column's sort                                  |
| `align`       | `'start' \| 'center' \| 'end'` | —        | Horizontal alignment of header and body cells                                       |
| `footer`      | `string`                       | —        | Footer cell copy. A `#footer-{key}` slot overrides this                             |
| `minWidth`    | `string`                       | —        | Minimum column width (CSS length)                                                   |
| `width`       | `string`                       | —        | Column width. Used as `minWidth` when `minWidth` is omitted                         |
| `frozen`      | `boolean`                      | `false`  | Pins the column during horizontal scroll                                            |
| `alignFrozen` | `'left' \| 'right'`            | `'left'` | Edge to pin a frozen column                                                         |

## Slots

| Name                        | Description                                                     |
| --------------------------- | --------------------------------------------------------------- |
| default                     | `<AzColumn>` children that declare the columns                  |
| `header`                    | Content rendered above the table (freeform; prefer `toolbar`)   |
| `footer`                    | Content rendered below the table                                |
| `empty`                     | Replaces the whole empty state (title, description, and action) |
| `empty-action`              | Replaces the built-in empty-state action                        |
| `loading`                   | Replaces the default loading overlay copy                       |
| `cell-{key}`                | Per-column body cell. Scope: `{ row, column, value, index }`    |
| `header-{key}`              | Per-column header cell. Scope: `{ column }`                     |
| `footer-{key}`              | Per-column footer cell. Scope: `{ column }`                     |
| `expansion`                 | Content under an expanded row. Scope: `{ row, index }`          |
| `toolbar-toggle`            | Optional control in the toolbar end cluster (e.g. a switch)     |
| `toolbar-search`            | Replaces the built-in search field                              |
| `toolbar-actions`           | Filter, settings, primary, and negative actions                 |
| `toolbar-selection-actions` | Icons or controls next to the selection count                   |
| `toolbar-start`             | Replaces the toolbar start cluster                              |
| `toolbar-end`               | Replaces the toolbar end cluster                                |
| `toolbar-tags`              | Replaces the built-in filter tags row                           |

## Events

| Name                   | Payload                                                                   | Description                                           |
| ---------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------- |
| `update:sortField`     | `[string \| undefined]`                                                   | Emitted when the sorted column changes                |
| `update:sortOrder`     | `[1 \| -1 \| null]`                                                       | Emitted when the sort direction changes               |
| `update:multiSortMeta` | `[DataTableSortMeta[]]`                                                   | Emitted when multi-column sort keys change            |
| `sort`                 | `[{ field, order, multiSortMeta? }]`                                      | Emitted whenever sort changes                         |
| `row-click`            | `[{ row, index, event }]`                                                 | Emitted when a body row is clicked                    |
| `update:selection`     | `[DataTableSelection]`                                                    | Emitted when the selected row or rows change          |
| `row-select`           | `[{ row, index }]`                                                        | Emitted when a row becomes selected                   |
| `row-unselect`         | `[{ row, index }]`                                                        | Emitted when a row is removed from the selection      |
| `update:expandedRows`  | `[DataTableRow[]]`                                                        | Emitted when the expanded rows change                 |
| `row-expand`           | `[{ row, index }]`                                                        | Emitted when a row is expanded                        |
| `row-collapse`         | `[{ row, index }]`                                                        | Emitted when a row is collapsed                       |
| `update:first`         | `[number]`                                                                | Emitted when the first-row index changes              |
| `update:rows`          | `[number]`                                                                | Emitted when rows per page change                     |
| `page`                 | `[{ first, rows, page, pageCount, sortField, sortOrder, multiSortMeta }]` | Emitted when pagination changes. `page` is 0-based    |
| `update:globalFilter`  | `[string]`                                                                | Emitted when the toolbar search value changes         |
| `select-all-in-table`  | `[]`                                                                      | Emitted when the banner selects every row             |
| `clear-selection`      | `[]`                                                                      | Emitted when the banner clears the selection          |
| `remove-filter`        | `[string]`                                                                | Emitted with the filter tag `id` on dismiss           |
| `reset-filters`        | `[]`                                                                      | Emitted when the filter-tags Reset control is clicked |
| `empty-action`         | `[]`                                                                      | Emitted when the built-in empty action is clicked     |

## Basic usage

```vue
<script setup>
import { AzDataTable, AzColumn } from '@ptlm-azulejo/data-table'

const value = [
  { code: 'f230fh0g3', name: 'Bamboo Watch', category: 'Accessories' },
  { code: 'nvklal433', name: 'Black Watch', category: 'Accessories' },
]
</script>

<template>
  <AzDataTable :value="value" data-key="code" caption="Products">
    <AzColumn field="code" header="Code" />
    <AzColumn field="name" header="Name" />
    <AzColumn field="category" header="Category" />
  </AzDataTable>
</template>
```

### Row expansion

```vue
<script setup>
import { ref } from 'vue'
import { AzDataTable, AzColumn } from '@ptlm-azulejo/data-table'

const expandedRows = ref([])
</script>

<template>
  <AzDataTable :value="value" data-key="code" expandable v-model:expanded-rows="expandedRows">
    <AzColumn field="code" header="Code" />
    <AzColumn field="name" header="Name" />
    <template #expansion="{ row }">
      <p>{{ row.name }}</p>
    </template>
  </AzDataTable>
</template>
```

### Toolbar

```vue
<script setup>
import { ref } from 'vue'
import { AzDataTable, AzColumn } from '@ptlm-azulejo/data-table'

const selection = ref([])
const globalFilter = ref('')
const filters = ref([{ id: 'accessories', label: 'Accessories' }])
</script>

<template>
  <AzDataTable
    :value="value"
    data-key="code"
    toolbar
    selection-mode="multiple"
    v-model:selection="selection"
    v-model:global-filter="globalFilter"
    :filters="filters"
    paginator
    :rows="10"
    @remove-filter="(id) => (filters = filters.filter((tag) => tag.id !== id))"
    @reset-filters="filters = []"
  >
    <AzColumn field="code" header="Code" />
    <AzColumn field="name" header="Name" />
    <AzColumn field="category" header="Category" />
    <template #toolbar-actions>
      <button type="button">Filter</button>
    </template>
  </AzDataTable>
</template>
```
