@tanstack/ember-table
Version:
Headless UI for building powerful tables & datagrids for Ember.
161 lines (122 loc) • 5.96 kB
Markdown
---
name: getting-started
description: >
Create a TanStack Ember Table v9 table with useTable, a tracked options thunk, stable tableFeatures and columns, .gts templates, FlexRenderCell/Header/Footer, and correctly bound template helpers. Load for first-table setup, Glimmer reactivity, component cell renderers, or adapting another framework's example to Ember.
metadata:
type: framework
library: '@tanstack/ember-table'
framework: ember
library_version: '9.0.0-beta.80'
requires:
- '@tanstack/table-core#core'
- '@tanstack/table-core#table-features'
sources:
- 'TanStack/table:docs/framework/ember/quick-start.md'
- 'TanStack/table:examples/ember/basic-table'
- 'TanStack/table:packages/ember-table/src/index.ts'
- 'TanStack/table:packages/ember-table/src/use-table.ts'
- 'TanStack/table:packages/ember-table/src/FlexRender.gts'
---
This skill builds on `@tanstack/table-core#core` and `@tanstack/table-core#table-features`. Ember Table is headless: it supplies reactive table models and renderer helpers, while the application owns semantic markup, CSS, accessibility, and design-system components.
The v9 addon requires Ember 5.8 or newer with Embroider or ember-auto-import v2. Prefer `.gts`/`.gjs` template-tag components with Glint.
## Setup
```gts
import Component from '@glimmer/component'
import { tracked } from '@glimmer/tracking'
import { on } from '@ember/modifier'
import {
FlexRenderCell,
FlexRenderHeader,
createColumnHelper,
tableFeatures,
useTable,
type Cell,
type Row,
} from '@tanstack/ember-table'
type Person = { id: string; name: string }
const features = tableFeatures({})
const columnHelper = createColumnHelper<typeof features, Person>()
const columns = columnHelper.columns([
columnHelper.accessor('name', { header: 'Name' }),
])
const initialData: Person[] = [{ id: '1', name: 'Ada' }]
const getRowId = (row: Person) => row.id
const getAllCells = (
row: Row<typeof features, Person>,
): Array<Cell<typeof features, Person>> => row.getAllCells()
export default class PeopleTable extends Component {
@tracked data = initialData
table = useTable(() => ({
features,
columns,
data: this.data,
getRowId,
}))
get headerGroups() {
return this.table.getHeaderGroups()
}
get rows() {
return this.table.getRowModel().rows
}
addPerson = () => {
this.data = [...this.data, { id: '2', name: 'Grace' }]
}
<template>
<button type='button' {{on 'click' this.addPerson}}>Add person</button>
<table>
<thead>
{{#each this.headerGroups as |group|}}
<tr>
{{#each group.headers as |header|}}
<th colspan={{header.colSpan}}>
{{#unless header.isPlaceholder}}
<FlexRenderHeader @header={{header}} />
{{/unless}}
</th>
{{/each}}
</tr>
{{/each}}
</thead>
<tbody>
{{#each this.rows as |row|}}
<tr>
{{#each (getAllCells row) as |cell|}}
<td><FlexRenderCell @cell={{cell}} /></td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
</template>
}
```
`useTable` takes an options thunk. Tracked properties read by that thunk update table options, and table API reads inside getters/templates participate in Glimmer tracking. Keep `features` and `columns` at stable module or component-lifetime scope; replace `@tracked data` only when data meaningfully changes.
## Ember-Specific Patterns
### Preserve method receivers in templates
V9 table, column, row, cell, and header methods live on prototypes and require their receiver. Ember templates extract function references, so call methods in a getter or a small module-level helper:
```gts
const getCanSort = (column: Column<typeof features, Person>) =>
column.getCanSort()
const toggleSort =
(column: Column<typeof features, Person>) => (event: Event) =>
column.getToggleSortingHandler()?.(event)
```
Pass `toggleSort header.column` to `{{on}}`; do not pass an extracted Table method directly.
### Render definitions through the matching component
Use `FlexRenderCell`, `FlexRenderHeader`, and `FlexRenderFooter` with their matching object. A definition may return a primitive or `flexRenderComponent(Component, options)`. The rendered component receives `@ctx` and optional `@options`; Table does not instantiate arbitrary component-library markup for you.
## Common Mistakes
### HIGH Passing an options object instead of a thunk
Wrong: `useTable({ features, columns, data: this.data })`.
Correct: `useTable(() => ({ features, columns, data: this.data }))`.
The thunk is how tracked option reads are connected to the table.
### HIGH Passing an unbound prototype method to a template modifier
Wrong: `{{on 'click' header.column.getToggleSortingHandler}}`.
Correct: wrap the call in a helper that invokes the method on `header.column`, as shown above.
An extracted v9 prototype method loses `this` and can throw or silently target the wrong receiver.
### HIGH Recreating model inputs inside the options thunk
Wrong: `columns: columnHelper.columns(...)` or `data: this.data.slice()` inside `useTable(() => ...)`.
Correct: keep columns stable and pass the tracked data reference directly. The thunk may rerun; it should not manufacture new model inputs on every tracked update.
### MEDIUM Using a visibility-aware API without its feature
With `tableFeatures({})`, render `row.getAllCells()`. Add `columnVisibilityFeature` before using `row.getVisibleCells()`.
## API Discovery
Inspect `node_modules/@tanstack/ember-table/declarations/index.d.ts` for exports, `use-table.d.ts` for options/reactivity behavior, `FlexRender.d.ts` and `flex-render.d.ts` for renderer contracts, and `node_modules/@tanstack/table-core/dist/features/<feature>/` for feature-gated APIs. Do not substitute React hooks, subscriptions, or component signatures.