- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Questionnaire
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner (Toast)
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
Data Table
Powerful table and datagrids built using TanStack Table.
Status | Amount | |||
|---|---|---|---|---|
success | ken99@yahoo.com | $316.00 | ||
success | Abe45@gmail.com | $242.00 | ||
processing | Monserrat44@gmail.com | $837.00 | ||
success | Silas22@gmail.com | $874.00 | ||
failed | carmella@hotmail.com | $721.00 |
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideChevronDown } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmTableImports } from '@spartan-ng/helm/table';
import { hlmMuted } from '@spartan-ng/helm/typography';
import {
columnFilteringFeature,
type ColumnFiltersState,
columnVisibilityFeature,
type ColumnVisibilityState,
createColumnHelper,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFn_includesString,
FlexRender,
injectTable,
rowPaginationFeature,
rowSelectionFeature,
type RowSelectionState,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_text,
type SortingState,
tableFeatures,
} from '@tanstack/angular-table';
import { ActionDropdown } from './action-dropdown';
import { TableHeadSelection, TableRowSelection } from './selection-column';
import { TableHeadSortButton } from './sort-header-button';
export type Payment = {
id: string;
amount: number;
status: 'pending' | 'processing' | 'success' | 'failed';
email: string;
};
const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
});
export type DataTableFeatures = typeof features;
const columnHelper = createColumnHelper<DataTableFeatures, Payment>();
const columns = columnHelper.columns([
columnHelper.display({
id: 'select',
header: () => TableHeadSelection,
cell: () => TableRowSelection,
enableHiding: false,
}),
columnHelper.accessor('status', {
id: 'status',
header: 'Status',
cell: (info) => `<span class="capitalize">${info.getValue<string>()}</span>`,
}),
columnHelper.accessor('email', {
id: 'email',
header: () => TableHeadSortButton,
cell: (info) => `<div class="lowercase">${info.getValue<string>()}</div>`,
}),
columnHelper.accessor('amount', {
id: 'amount',
header: '<div class="text-right">Amount</div>',
cell: (info) => {
const amount = parseFloat(info.getValue<string>());
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
return `<div class="text-right font-medium">${formatted}</div>`;
},
}),
columnHelper.display({
id: 'actions',
cell: () => ActionDropdown,
enableHiding: false,
}),
]);
@Component({
selector: 'spartan-data-table-preview',
imports: [
FlexRender,
FormsModule,
HlmDropdownMenuImports,
HlmButtonImports,
NgIcon,
HlmInputImports,
HlmTableImports,
],
providers: [provideIcons({ lucideChevronDown })],
host: {
class: 'w-full',
},
template: `
<div class="flex flex-col justify-between gap-4 py-4 sm:flex-row sm:items-center">
<input hlmInput class="w-full md:w-80" placeholder="Filter emails..." (input)="_filterChanged($event)" />
<button hlmBtn variant="outline" align="end" [hlmDropdownMenuTrigger]="menu">
Columns
<ng-icon name="lucideChevronDown" class="ml-2" />
</button>
<ng-template #menu>
<hlm-dropdown-menu class="w-32">
@for (column of _hidableColumns; track column.id) {
<button
hlmDropdownMenuCheckbox
class="capitalize"
[checked]="column.getIsVisible()"
(triggered)="column.toggleVisibility()"
>
<hlm-dropdown-menu-checkbox-indicator />
{{ column.columnDef.id }}
</button>
}
</hlm-dropdown-menu>
</ng-template>
</div>
<div class="overflow-hidden rounded-md border">
<div hlmTableContainer>
<table hlmTable>
<thead hlmTHead>
@for (headerGroup of _table.getHeaderGroups(); track headerGroup.id) {
<tr hlmTr>
@for (header of headerGroup.headers; track header.id) {
<th hlmTh [attr.colSpan]="header.colSpan">
@if (!header.isPlaceholder) {
<ng-container
*flexRender="header.column.columnDef.header; props: header.getContext(); let headerText"
>
<div [innerHTML]="headerText"></div>
</ng-container>
}
</th>
}
</tr>
}
</thead>
<tbody hlmTBody>
@for (row of _table.getRowModel().rows; track row.id) {
<tr hlmTr [attr.key]="row.id" [attr.data-state]="row.getIsSelected() && 'selected'">
@for (cell of row.getVisibleCells(); track $index) {
<td hlmTd>
<ng-container *flexRender="cell.column.columnDef.cell; props: cell.getContext(); let cell">
<div [innerHTML]="cell"></div>
</ng-container>
</td>
}
</tr>
} @empty {
<tr hlmTr>
<td hlmTd class="h-24 text-center" [attr.colspan]="_columns.length">No results.</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="flex flex-col justify-between py-4 sm:flex-row sm:items-center">
@if (_table.getRowCount() > 0) {
<div class="${hlmMuted}">
{{ _table.getSelectedRowModel().rows.length }} of {{ _table.getRowCount() }} row(s) selected
</div>
<div class="mt-2 flex space-x-2 sm:mt-0">
<button
size="sm"
variant="outline"
hlmBtn
[disabled]="!_table.getCanPreviousPage()"
(click)="_table.previousPage()"
>
Previous
</button>
<button size="sm" variant="outline" hlmBtn [disabled]="!_table.getCanNextPage()" (click)="_table.nextPage()">
Next
</button>
</div>
} @else {
<div class="flex h-full w-full items-center justify-center">
<div class="text-muted-foreground text-sm">No Data</div>
</div>
}
</div>
`,
})
export class DataTablePreview {
protected readonly _columns = columns;
private readonly _columnFilters = signal<ColumnFiltersState>([]);
private readonly _sorting = signal<SortingState>([]);
private readonly _rowSelection = signal<RowSelectionState>({});
private readonly _columnVisibility = signal<ColumnVisibilityState>({});
protected readonly _table = injectTable(() => ({
features,
columns,
data: PAYMENT_DATA,
onSortingChange: (updater) => {
updater instanceof Function ? this._sorting.update(updater) : this._sorting.set(updater);
},
onColumnFiltersChange: (updater) => {
updater instanceof Function ? this._columnFilters.update(updater) : this._columnFilters.set(updater);
},
onColumnVisibilityChange: (updater) => {
updater instanceof Function ? this._columnVisibility.update(updater) : this._columnVisibility.set(updater);
},
onRowSelectionChange: (updater) => {
updater instanceof Function ? this._rowSelection.update(updater) : this._rowSelection.set(updater);
},
state: {
sorting: this._sorting(),
columnFilters: this._columnFilters(),
columnVisibility: this._columnVisibility(),
rowSelection: this._rowSelection(),
},
}));
protected readonly _hidableColumns = this._table.getAllColumns().filter((column) => column.getCanHide());
protected _filterChanged(event: Event) {
this._table.getColumn('email')?.setFilterValue((event.target as HTMLInputElement).value);
}
}
const PAYMENT_DATA: Payment[] = [
{
id: 'm5gr84i9',
amount: 316,
status: 'success',
email: 'ken99@yahoo.com',
},
{
id: '3u1reuv4',
amount: 242,
status: 'success',
email: 'Abe45@gmail.com',
},
{
id: 'derv1ws0',
amount: 837,
status: 'processing',
email: 'Monserrat44@gmail.com',
},
{
id: '5kma53ae',
amount: 874,
status: 'success',
email: 'Silas22@gmail.com',
},
{
id: 'bhqecj4p',
amount: 721,
status: 'failed',
email: 'carmella@hotmail.com',
},
];Introduction
Every data table or datagrid tends to be unique. They all behave differently, have specific sorting and filtering requirements, and work with different data sources.
It doesn't make sense to combine all of these variations into a single component. If we do that, we'll lose the flexibility that headless UI provides.
So instead of a data-table component, this page is a guide on how to build your own. We'll start with the basic Table directives and build a complex data table from scratch using TanStack Table .
Tip: If you find yourself using the same table in multiple places in your app, you can always extract it into a reusable component.
Installation
Add the Table directives to your project.
ng g @spartan-ng/cli:ui tablenx g @spartan-ng/cli:ui table Then add the @tanstack/angular-table dependency. This guide uses TanStack Table v9.
npm install @tanstack/angular-tablePrerequisites
We are going to build a table to show recent payments. Here's what our data looks like:
export type Payment = {
id: string;
amount: number;
status: 'pending' | 'processing' | 'success' | 'failed';
email: string;
};
export const payments: Payment[] = [
{
id: '728ed52f',
amount: 100,
status: 'pending',
email: 'm@example.com',
},
{
id: '489e1d42',
amount: 125,
status: 'processing',
email: 'example@gmail.com',
},
// ...
];Set up Table Features
TanStack Table v9 is feature-based: you opt into the behavior you want — sorting, filtering, pagination, and so on — by declaring it with tableFeatures() . Anything you don't list is tree-shaken out of your bundle. That includes the built-in filter and sort functions: register the ones your columns rely on under filterFns and sortFns (our email filter uses includesString , and string columns sort with text / alphanumeric ).
import {
columnFilteringFeature,
columnVisibilityFeature,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFn_includesString,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_text,
tableFeatures,
} from '@tanstack/angular-table';
// New in v9: declare the features this table uses — anything you don't
// register is tree-shaken out of the bundle.
export const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
});
// Pass this as the first generic argument to `ColumnDef`, `Column`, `Table`
// and `Row` so each type knows which feature APIs are available.
export type DataTableFeatures = typeof features;Note: The core row model is always included, so you never register it yourself. Row models for optional features are created with create*RowModel() and registered on the features object — there are no more get*RowModel table options.
Basic Table
Let's start by building a basic table.
Column definitions
First, we'll define our columns.
import { createColumnHelper } from '@tanstack/angular-table';
import { type DataTableFeatures } from './data-table-features';
import { type Payment } from './payments';
// Use `accessor` for data columns and `display` for columns without one.
const columnHelper = createColumnHelper<DataTableFeatures, Payment>();
export const columns = columnHelper.columns([
columnHelper.accessor('status', {
header: 'Status',
}),
columnHelper.accessor('email', {
header: 'Email',
}),
columnHelper.accessor('amount', {
header: 'Amount',
}),
]);Note: Columns are where you define the core of what your table will look like. They define the data that will be displayed, how it will be formatted, sorted and filtered.
Data table component
Next, we'll create a DataTable component that receives the columns and data as inputs. We create the table instance with injectTable() , passing it our features together with those inputs, and render headers and cells with the *flexRender directive.
import { Component, input } from '@angular/core';
import { HlmTableImports } from '@spartan-ng/helm/table';
import { type ColumnDef, FlexRender, injectTable, type RowData } from '@tanstack/angular-table';
import { features, type DataTableFeatures } from './data-table-features';
@Component({
selector: 'app-data-table',
imports: [FlexRender, HlmTableImports],
template: `
<div class="overflow-hidden rounded-md border">
<div hlmTableContainer>
<table hlmTable>
<thead hlmTHead>
@for (headerGroup of table.getHeaderGroups(); track headerGroup.id) {
<tr hlmTr>
@for (header of headerGroup.headers; track header.id) {
<th hlmTh [attr.colSpan]="header.colSpan">
@if (!header.isPlaceholder) {
<ng-container
*flexRender="header.column.columnDef.header; props: header.getContext(); let headerText"
>
<div [innerHTML]="headerText"></div>
</ng-container>
}
</th>
}
</tr>
}
</thead>
<tbody hlmTBody>
@for (row of table.getRowModel().rows; track row.id) {
<tr hlmTr [attr.data-state]="row.getIsSelected() && 'selected'">
@for (cell of row.getVisibleCells(); track $index) {
<td hlmTd>
<ng-container *flexRender="cell.column.columnDef.cell; props: cell.getContext(); let cellText">
<div [innerHTML]="cellText"></div>
</ng-container>
</td>
}
</tr>
} @empty {
<tr hlmTr>
<td hlmTd class="h-24 text-center" [attr.colspan]="columns().length">No results.</td>
</tr>
}
</tbody>
</table>
</div>
</div>
`,
})
export class DataTable<TData extends RowData> {
public readonly columns = input.required<ColumnDef<DataTableFeatures, TData>[]>();
public readonly data = input.required<TData[]>();
protected readonly table = injectTable(() => ({
features,
columns: this.columns(),
data: this.data(),
}));
}Rendering with FlexRender: A column's header and cell definitions can return a plain string, an HTML string (rendered above via [innerHTML] ) or an Angular component class — we'll use components for the sorting button, the selection checkboxes and the actions dropdown later in this guide.
Render the table
Finally, we'll render our table in our page component.
import { Component, signal } from '@angular/core';
import { columns } from './columns';
import { DataTable } from './data-table';
import { type Payment } from './payments';
@Component({
selector: 'app-demo-page',
imports: [DataTable],
template: `
<div class="container mx-auto py-10">
<app-data-table [columns]="columns" [data]="data()" />
</div>
`,
})
export class DemoPage {
protected readonly columns = columns;
// Fetch data from your API here.
protected readonly data = signal<Payment[]>([
{
id: '728ed52f',
amount: 100,
status: 'pending',
email: 'm@example.com',
},
// ...
]);
} The page component owns the data: fetch it from your API (e.g. with httpResource or a service) and pass it to the table through the data input. Inputs are signals and the injectTable() options callback is reactive, so the table updates automatically whenever the data changes.
Cell Formatting
Let's format the amount cell to display the dollar amount. We'll also align the cell to the right. Update the header and cell definitions for amount as follows:
export const columns = columnHelper.columns([
columnHelper.accessor('amount', {
header: '<div class="text-right">Amount</div>',
cell: (info) => {
const amount = parseFloat(info.getValue<string>());
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
}).format(amount);
return `<div class="text-right font-medium">${formatted}</div>`;
},
}),
]);You can use the same approach to format other cells and headers.
Row Actions
Let's add row actions to our table. We'll use a Dropdown Menu for this and create a component that renders it inside a cell. The component receives the row as an input, so you can access the row data using row().original — use this to handle actions for your row, e.g. use the id to make a DELETE call to your API.
import { Component, input } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideEllipsis } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
import type { Row } from '@tanstack/angular-table';
import { type DataTableFeatures } from './data-table-features';
import { type Payment } from './payments';
@Component({
selector: 'app-action-dropdown',
imports: [HlmButtonImports, NgIcon, HlmDropdownMenuImports],
providers: [provideIcons({ lucideEllipsis })],
template: `
<button hlmBtn size="icon-sm" variant="ghost" [hlmDropdownMenuTrigger]="ActionDropDownMenu">
<span class="sr-only">Open menu</span>
<ng-icon name="lucideEllipsis" />
</button>
<ng-template #ActionDropDownMenu>
<hlm-dropdown-menu>
<hlm-dropdown-menu-label>Actions</hlm-dropdown-menu-label>
<button hlmDropdownMenuItem (click)="copyPaymentId()">Copy payment ID</button>
<hlm-dropdown-menu-separator />
<button hlmDropdownMenuItem>View customer</button>
<button hlmDropdownMenuItem>View payment details</button>
</hlm-dropdown-menu>
</ng-template>
`,
})
export class ActionDropdown {
public readonly row = input.required<Row<DataTableFeatures, Payment>>();
copyPaymentId() {
const payment = this.row().original;
navigator.clipboard.writeText(payment.id);
}
} Then update our columns definition to add a new actions display column that renders the component:
import { ActionDropdown } from './action-dropdown';
export const columns = columnHelper.columns([
// ...
columnHelper.display({
id: 'actions',
cell: () => ActionDropdown,
enableHiding: false,
}),
]);Pagination
Because our features object includes rowPaginationFeature and createPaginatedRowModel() , the table automatically paginates rows into pages of 10 — there's nothing to add to injectTable . We can add pagination controls to our table using the Button component and the table.previousPage() and table.nextPage() API methods.
import { HlmButtonImports } from '@spartan-ng/helm/button';
@Component({
selector: 'app-data-table',
imports: [FlexRender, HlmButtonImports, HlmTableImports],
template: `
<div class="overflow-hidden rounded-md border">
<!-- table -->
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<button
hlmBtn
variant="outline"
size="sm"
[disabled]="!table.getCanPreviousPage()"
(click)="table.previousPage()"
>
Previous
</button>
<button hlmBtn variant="outline" size="sm" [disabled]="!table.getCanNextPage()" (click)="table.nextPage()">
Next
</button>
</div>
`,
})
export class DataTable<TData extends RowData> {
// ...
}See the pagination docs for more information on customizing page size and implementing manual pagination.
Sorting
Let's make the email column sortable. The rowSortingFeature and sorted row model are already registered in our features object, so all that's left is wiring up the sorting state.
Wire up the sorting state
We hold the sorting state in a signal and connect it to the table via state and onSortingChange :
import { Component, input, signal } from '@angular/core';
import { injectTable, isFunction, type SortingState } from '@tanstack/angular-table';
export class DataTable<TData extends RowData> {
private readonly _sorting = signal<SortingState>([]);
protected readonly table = injectTable(() => ({
features,
columns: this.columns(),
data: this.data(),
onSortingChange: (updater) => (isFunction(updater) ? this._sorting.update(updater) : this._sorting.set(updater)),
state: {
sorting: this._sorting(),
},
}));
}Make header cell sortable
We create a header button component that toggles sorting for its column:
import { Component, input } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideArrowUpDown } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { type Column } from '@tanstack/angular-table';
import { type DataTableFeatures } from './data-table-features';
import { type Payment } from './payments';
@Component({
imports: [HlmButtonImports, NgIcon],
providers: [provideIcons({ lucideArrowUpDown })],
template: `
<button hlmBtn size="sm" variant="ghost" class="capitalize" (click)="sortClick()">
{{ column().id }}
<ng-icon name="lucideArrowUpDown" />
</button>
`,
})
export class TableHeadSortButton {
public readonly column = input.required<Column<DataTableFeatures, Payment, unknown>>();
protected sortClick() {
this.column().toggleSorting(this.column().getIsSorted() === 'asc');
}
} And render it as the email column's header:
import { TableHeadSortButton } from './sort-header-button';
export const columns = columnHelper.columns([
columnHelper.accessor('email', {
header: () => TableHeadSortButton,
cell: (info) => `<div class="lowercase">${info.getValue<string>()}</div>`,
}),
]);This will automatically sort the table (asc and desc) when the user toggles on the header cell.
Filtering
Let's add a search input to filter emails in our table. The columnFilteringFeature and filtered row model are already part of our features object, so we only need to wire up the filter state and render an Input .
import { Component, input, signal } from '@angular/core';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { type ColumnFiltersState, injectTable, isFunction } from '@tanstack/angular-table';
@Component({
selector: 'app-data-table',
imports: [FlexRender, HlmButtonImports, HlmInputImports, HlmTableImports],
template: `
<div class="flex items-center py-4">
<input hlmInput class="w-full md:w-80" placeholder="Filter emails..." (input)="filterChanged($event)" />
</div>
<div class="overflow-hidden rounded-md border">
<!-- table -->
</div>
`,
})
export class DataTable<TData extends RowData> {
private readonly _columnFilters = signal<ColumnFiltersState>([]);
protected readonly table = injectTable(() => ({
features,
columns: this.columns(),
data: this.data(),
onColumnFiltersChange: (updater) =>
isFunction(updater) ? this._columnFilters.update(updater) : this._columnFilters.set(updater),
state: {
columnFilters: this._columnFilters(),
},
}));
protected filterChanged(event: Event) {
this.table.getColumn('email')?.setFilterValue((event.target as HTMLInputElement).value);
}
} Filtering is now enabled for the email column. You can add filters to other columns as well. See the filtering docs for more information on customizing filters.
Visibility
Adding column visibility is fairly simple using the visibility API. We add a Dropdown Menu that toggles the visibility of every column that can be hidden:
import { Component, input, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideChevronDown } from '@ng-icons/lucide';
import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
import { type ColumnVisibilityState, injectTable, isFunction } from '@tanstack/angular-table';
@Component({
selector: 'app-data-table',
imports: [FlexRender, HlmButtonImports, HlmDropdownMenuImports, HlmInputImports, HlmTableImports, NgIcon],
providers: [provideIcons({ lucideChevronDown })],
template: `
<div class="flex items-center py-4">
<!-- filter input -->
<button hlmBtn variant="outline" align="end" class="ml-auto" [hlmDropdownMenuTrigger]="menu">
Columns
<ng-icon name="lucideChevronDown" class="ml-2" />
</button>
<ng-template #menu>
<hlm-dropdown-menu class="w-32">
@for (column of hidableColumns; track column.id) {
<button
hlmDropdownMenuCheckbox
class="capitalize"
[checked]="column.getIsVisible()"
(triggered)="column.toggleVisibility()"
>
<hlm-dropdown-menu-checkbox-indicator />
{{ column.columnDef.id }}
</button>
}
</hlm-dropdown-menu>
</ng-template>
</div>
<div class="overflow-hidden rounded-md border">
<!-- table -->
</div>
`,
})
export class DataTable<TData extends RowData> {
private readonly _columnVisibility = signal<ColumnVisibilityState>({});
protected readonly table = injectTable(() => ({
features,
columns: this.columns(),
data: this.data(),
onColumnVisibilityChange: (updater) =>
isFunction(updater) ? this._columnVisibility.update(updater) : this._columnVisibility.set(updater),
state: {
columnVisibility: this._columnVisibility(),
},
}));
protected readonly hidableColumns = this.table.getAllColumns().filter((column) => column.getCanHide());
}This adds a dropdown menu that you can use to toggle column visibility.
Row Selection
Next, we're going to add row selection to our table.
Create selection components
We create two small Checkbox components: one for the header to select all rows on the page and one for each row.
import { Component, input } from '@angular/core';
import { HlmCheckboxImports } from '@spartan-ng/helm/checkbox';
import { type Row, type Table } from '@tanstack/angular-table';
import { type DataTableFeatures } from './data-table-features';
import { type Payment } from './payments';
@Component({
imports: [HlmCheckboxImports],
host: {
class: 'flex',
'aria-label': 'Select all',
},
template: `
<hlm-checkbox
[checked]="table().getIsAllRowsSelected()"
[indeterminate]="table().getIsSomeRowsSelected() && !table().getIsAllPageRowsSelected()"
(checkedChange)="table().toggleAllPageRowsSelected($event)"
/>
`,
})
export class TableHeadSelection {
public readonly table = input.required<Table<DataTableFeatures, Payment>>();
}
@Component({
imports: [HlmCheckboxImports],
host: {
class: 'flex',
'aria-label': 'Select Row',
},
template: `
<hlm-checkbox [checked]="row().getIsSelected()" (checkedChange)="row().toggleSelected($event)" />
`,
})
export class TableRowSelection {
public readonly row = input.required<Row<DataTableFeatures, Payment>>();
} Then add a select display column that renders them:
import { TableHeadSelection, TableRowSelection } from './selection-column';
export const columns = columnHelper.columns([
columnHelper.display({
id: 'select',
header: () => TableHeadSelection,
cell: () => TableRowSelection,
enableHiding: false,
}),
// ...
]);Wire up the selection state
import { Component, input, signal } from '@angular/core';
import { injectTable, isFunction, type RowSelectionState } from '@tanstack/angular-table';
export class DataTable<TData extends RowData> {
private readonly _rowSelection = signal<RowSelectionState>({});
protected readonly table = injectTable(() => ({
features,
columns: this.columns(),
data: this.data(),
onRowSelectionChange: (updater) =>
isFunction(updater) ? this._rowSelection.update(updater) : this._rowSelection.set(updater),
state: {
rowSelection: this._rowSelection(),
},
}));
}This adds a checkbox to each row and a checkbox in the header to select all rows.
Show selected rows
You can show the number of selected rows using the table.getSelectedRowModel() API.
<div class="text-muted-foreground text-sm">
{{ table.getSelectedRowModel().rows.length }} of {{ table.getRowCount() }} row(s) selected.
</div>Reusable Components
Here are some components you can use to build your data tables. Components rendered inside a column (like the column header) receive their context from *flexRender . Components rendered outside the table (pagination, column toggle) read the table instance from DI with injectTableContext() , which is provided by wrapping them in the [tanStackTable] directive. Since no table input or generics are involved, the same components work with any of your tables.
Column header
Make any column header sortable and hideable.
import { Component, input } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideArrowDown, lucideArrowUp, lucideChevronsUpDown, lucideEyeOff } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
import { type Column } from '@tanstack/angular-table';
import { type DataTableFeatures } from './data-table-features';
import { type Payment } from './payments';
@Component({
selector: 'app-data-table-column-header',
imports: [HlmButtonImports, HlmDropdownMenuImports, NgIcon],
providers: [provideIcons({ lucideArrowDown, lucideArrowUp, lucideChevronsUpDown, lucideEyeOff })],
template: `
@if (column().getCanSort()) {
<div class="flex items-center gap-2">
<button
hlmBtn
variant="ghost"
size="sm"
class="data-[state=open]:bg-accent -ml-3 h-8"
align="start"
[hlmDropdownMenuTrigger]="menu"
>
<span>{{ title() }}</span>
@switch (column().getIsSorted()) {
@case ('desc') {
<ng-icon name="lucideArrowDown" />
}
@case ('asc') {
<ng-icon name="lucideArrowUp" />
}
@default {
<ng-icon name="lucideChevronsUpDown" />
}
}
</button>
<ng-template #menu>
<hlm-dropdown-menu>
<button hlmDropdownMenuItem (click)="column().toggleSorting(false)">
<ng-icon name="lucideArrowUp" />
Asc
</button>
<button hlmDropdownMenuItem (click)="column().toggleSorting(true)">
<ng-icon name="lucideArrowDown" />
Desc
</button>
<hlm-dropdown-menu-separator />
<button hlmDropdownMenuItem (click)="column().toggleVisibility(false)">
<ng-icon name="lucideEyeOff" />
Hide
</button>
</hlm-dropdown-menu>
</ng-template>
</div>
} @else {
<div>{{ title() }}</div>
}
`,
})
export class DataTableColumnHeader {
public readonly column = input.required<Column<DataTableFeatures, Payment, unknown>>();
public readonly title = input.required<string>();
} Since the component needs a title on top of the column from the flex-render context, register it with flexRenderComponent() , which lets you pass extra inputs:
import { flexRenderComponent } from '@tanstack/angular-table';
import { DataTableColumnHeader } from './data-table-column-header';
export const columns = columnHelper.columns([
columnHelper.accessor('email', {
header: ({ column }) => flexRenderComponent(DataTableColumnHeader, { inputs: { column, title: 'Email' } }),
}),
]);Pagination
Add pagination controls to your table including page size and selection count. The current page index and size are read from the signal-backed table.atoms.pagination atom, so the component stays in sync with the table.
import { Component, computed } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideChevronLeft, lucideChevronRight, lucideChevronsLeft, lucideChevronsRight } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmSelectImports } from '@spartan-ng/helm/select';
import { injectTableContext } from '@tanstack/angular-table';
@Component({
selector: 'app-data-table-pagination',
imports: [HlmButtonImports, HlmSelectImports, NgIcon],
providers: [provideIcons({ lucideChevronLeft, lucideChevronRight, lucideChevronsLeft, lucideChevronsRight })],
template: `
<div class="flex items-center justify-between px-2">
<div class="text-muted-foreground flex-1 text-sm">
{{ table().getFilteredSelectedRowModel().rows.length }} of
{{ table().getFilteredRowModel().rows.length }} row(s) selected.
</div>
<div class="flex items-center space-x-6 lg:space-x-8">
<div class="flex items-center space-x-2">
<p class="text-sm font-medium">Rows per page</p>
<hlm-select [value]="pagination().pageSize" (valueChange)="setPageSize($event)">
<hlm-select-trigger class="h-8 w-[70px]">
<hlm-select-value />
</hlm-select-trigger>
<hlm-select-content *hlmSelectPortal>
@for (pageSize of pageSizes; track pageSize) {
<hlm-select-item [value]="pageSize">{{ pageSize }}</hlm-select-item>
}
</hlm-select-content>
</hlm-select>
</div>
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
Page {{ pagination().pageIndex + 1 }} of {{ table().getPageCount() }}
</div>
<div class="flex items-center space-x-2">
<button
hlmBtn
variant="outline"
size="icon"
class="hidden size-8 lg:flex"
[disabled]="!table().getCanPreviousPage()"
(click)="table().setPageIndex(0)"
>
<span class="sr-only">Go to first page</span>
<ng-icon name="lucideChevronsLeft" />
</button>
<button
hlmBtn
variant="outline"
size="icon"
class="size-8"
[disabled]="!table().getCanPreviousPage()"
(click)="table().previousPage()"
>
<span class="sr-only">Go to previous page</span>
<ng-icon name="lucideChevronLeft" />
</button>
<button
hlmBtn
variant="outline"
size="icon"
class="size-8"
[disabled]="!table().getCanNextPage()"
(click)="table().nextPage()"
>
<span class="sr-only">Go to next page</span>
<ng-icon name="lucideChevronRight" />
</button>
<button
hlmBtn
variant="outline"
size="icon"
class="hidden size-8 lg:flex"
[disabled]="!table().getCanNextPage()"
(click)="table().setPageIndex(table().getPageCount() - 1)"
>
<span class="sr-only">Go to last page</span>
<ng-icon name="lucideChevronsRight" />
</button>
</div>
</div>
</div>
`,
})
export class DataTablePagination {
// Provided by the nearest `[tanStackTable]` directive, so no table input
// or generics are needed and this component works with any table.
protected readonly table = injectTableContext();
protected readonly pageSizes = [10, 20, 25, 30, 40, 50];
// Table atoms are backed by Angular signals, so this stays in sync.
protected readonly pagination = computed(() => this.table().atoms.pagination.get());
protected setPageSize(value: unknown) {
this.table().setPageSize(Number(value));
}
} Add TanStackTable to the imports of the component that owns the table and wrap the pagination in the [tanStackTable] directive:
import { TanStackTable } from '@tanstack/angular-table';
import { DataTablePagination } from './data-table-pagination';
@Component({
selector: 'app-data-table',
imports: [DataTablePagination, FlexRender, HlmTableImports, TanStackTable],
template: `
<div class="overflow-hidden rounded-md border">
<!-- table -->
</div>
<div class="py-4" [tanStackTable]="table">
<app-data-table-pagination />
</div>
`,
})
export class DataTable<TData extends RowData> {
// ...
}Column toggle
A component to toggle column visibility. Like the pagination component, it reads the table from the nearest [tanStackTable] directive.
import { Component, computed } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideSettings2 } from '@ng-icons/lucide';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
import { injectTableContext } from '@tanstack/angular-table';
@Component({
selector: 'app-data-table-view-options',
imports: [HlmButtonImports, HlmDropdownMenuImports, NgIcon],
providers: [provideIcons({ lucideSettings2 })],
template: `
<button
hlmBtn
variant="outline"
size="sm"
class="ml-auto hidden h-8 lg:flex"
align="end"
[hlmDropdownMenuTrigger]="menu"
>
<ng-icon name="lucideSettings2" />
View
</button>
<ng-template #menu>
<hlm-dropdown-menu class="w-[150px]">
<hlm-dropdown-menu-label>Toggle columns</hlm-dropdown-menu-label>
<hlm-dropdown-menu-separator />
@for (column of hidableColumns(); track column.id) {
<button
hlmDropdownMenuCheckbox
class="capitalize"
[checked]="column.getIsVisible()"
(triggered)="column.toggleVisibility()"
>
<hlm-dropdown-menu-checkbox-indicator />
{{ column.id }}
</button>
}
</hlm-dropdown-menu>
</ng-template>
`,
})
export class DataTableViewOptions {
protected readonly table = injectTableContext();
protected readonly hidableColumns = computed(() =>
this.table()
.getAllColumns()
.filter((column) => typeof column.accessorFn !== 'undefined' && column.getCanHide()),
);
}<div class="flex items-center py-4" [tanStackTable]="table">
<!-- filter input -->
<app-data-table-view-options />
</div>RTL
To enable RTL support in spartan-ng, see the RTL configuration guide.
الحالة | المبلغ | |||
|---|---|---|---|---|
ناجحة | ken99@yahoo.com | ٣١٦٫٠٠ US$ | ||
ناجحة | Abe45@gmail.com | ٢٤٢٫٠٠ US$ | ||
قيد المعالجة | Monserrat44@gmail.com | ٨٣٧٫٠٠ US$ | ||
ناجحة | Silas22@gmail.com | ٨٧٤٫٠٠ US$ | ||
فاشلة | carmella@hotmail.com | ٧٢١٫٠٠ US$ |
import { Directionality } from '@angular/cdk/bidi';
import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal, untracked } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { lucideArrowUpDown, lucideChevronDown, lucideEllipsis } from '@ng-icons/lucide';
import { TranslateService, Translations } from '@spartan-ng/app/app/shared/translate.service';
import { HlmButtonImports } from '@spartan-ng/helm/button';
import { HlmCheckboxImports } from '@spartan-ng/helm/checkbox';
import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
import { HlmInputImports } from '@spartan-ng/helm/input';
import { HlmTableImports } from '@spartan-ng/helm/table';
import { hlmMuted } from '@spartan-ng/helm/typography';
import {
type Column,
columnFilteringFeature,
type ColumnFiltersState,
columnVisibilityFeature,
type ColumnVisibilityState,
createColumnHelper,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
filterFn_includesString,
FlexRender,
injectTable,
isFunction,
type Row,
rowPaginationFeature,
rowSelectionFeature,
type RowSelectionState,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_text,
type SortingState,
type Table,
tableFeatures,
} from '@tanstack/angular-table';
type Payment = {
id: string;
amount: number;
status: 'pending' | 'processing' | 'success' | 'failed';
email: string;
};
const TRANSLATIONS: Translations = {
en: {
dir: 'ltr',
locale: 'en-US',
values: {
filter: 'Filter emails...',
columns: 'Columns',
status: 'Status',
email: 'Email',
amount: 'Amount',
pending: 'Pending',
processing: 'Processing',
success: 'Success',
failed: 'Failed',
actions: 'Actions',
openMenu: 'Open menu',
copyId: 'Copy payment ID',
viewCustomer: 'View customer',
viewPayment: 'View payment details',
noResults: 'No results.',
selected: '{selected} of {total} row(s) selected',
previous: 'Previous',
next: 'Next',
selectAll: 'Select all',
selectRow: 'Select row',
},
},
ar: {
dir: 'rtl',
locale: 'ar-EG',
values: {
filter: 'تصفية البريد الإلكتروني...',
columns: 'الأعمدة',
status: 'الحالة',
email: 'البريد الإلكتروني',
amount: 'المبلغ',
pending: 'قيد الانتظار',
processing: 'قيد المعالجة',
success: 'ناجحة',
failed: 'فاشلة',
actions: 'الإجراءات',
openMenu: 'افتح القائمة',
copyId: 'نسخ معرف الدفعة',
viewCustomer: 'عرض العميل',
viewPayment: 'عرض تفاصيل الدفعة',
noResults: 'لا توجد نتائج.',
selected: 'تم تحديد {selected} من أصل {total} صفوف',
previous: 'السابق',
next: 'التالي',
selectAll: 'تحديد الكل',
selectRow: 'تحديد الصف',
},
},
he: {
dir: 'rtl',
locale: 'he-IL',
values: {
filter: 'סינון אימיילים...',
columns: 'עמודות',
status: 'סטטוס',
email: 'אימייל',
amount: 'סכום',
pending: 'ממתין',
processing: 'בעיבוד',
success: 'הצליח',
failed: 'נכשל',
actions: 'פעולות',
openMenu: 'פתח תפריט',
copyId: 'העתק מזהה תשלום',
viewCustomer: 'הצג לקוח',
viewPayment: 'הצג פרטי תשלום',
noResults: 'אין תוצאות.',
selected: 'נבחרו {selected} מתוך {total} שורות',
previous: 'הקודם',
next: 'הבא',
selectAll: 'בחר הכל',
selectRow: 'בחר שורה',
},
},
};
const features = tableFeatures({
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSelectionFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { alphanumeric: sortFn_alphanumeric, text: sortFn_text },
});
type RtlDataTableFeatures = typeof features;
const columnHelper = createColumnHelper<RtlDataTableFeatures, Payment>();
@Component({
imports: [HlmCheckboxImports],
host: {
class: 'flex',
'[attr.aria-label]': '_t()["selectAll"]',
},
template: `
<hlm-checkbox
[checked]="table().getIsAllRowsSelected()"
[indeterminate]="table().getIsSomeRowsSelected() && !table().getIsAllPageRowsSelected()"
(checkedChange)="table().toggleAllPageRowsSelected($event)"
/>
`,
})
export class RtlTableHeadSelection {
public readonly table = input.required<Table<RtlDataTableFeatures, Payment>>();
private readonly _language = inject(TranslateService).language;
protected readonly _t = computed(() => TRANSLATIONS[this._language()].values);
}
@Component({
imports: [HlmCheckboxImports],
host: {
class: 'flex',
'[attr.aria-label]': '_t()["selectRow"]',
},
template: `
<hlm-checkbox [checked]="row().getIsSelected()" (checkedChange)="row().toggleSelected($event)" />
`,
})
export class RtlTableRowSelection {
public readonly row = input.required<Row<RtlDataTableFeatures, Payment>>();
private readonly _language = inject(TranslateService).language;
protected readonly _t = computed(() => TRANSLATIONS[this._language()].values);
}
@Component({
imports: [HlmButtonImports, NgIcon],
providers: [provideIcons({ lucideArrowUpDown })],
template: `
<button hlmBtn size="sm" variant="ghost" (click)="sortClick()">
{{ _t()['email'] }}
<ng-icon name="lucideArrowUpDown" />
</button>
`,
})
export class RtlTableHeadSortButton {
public readonly column = input.required<Column<RtlDataTableFeatures, Payment, unknown>>();
private readonly _language = inject(TranslateService).language;
protected readonly _t = computed(() => TRANSLATIONS[this._language()].values);
protected sortClick() {
this.column().toggleSorting(this.column().getIsSorted() === 'asc');
}
}
@Component({
imports: [HlmButtonImports, NgIcon, HlmDropdownMenuImports],
providers: [provideIcons({ lucideEllipsis })],
template: `
<button hlmBtn size="icon-sm" variant="ghost" [hlmDropdownMenuTrigger]="menu">
<span class="sr-only">{{ _t()['openMenu'] }}</span>
<ng-icon name="lucideEllipsis" />
</button>
<ng-template #menu>
<hlm-dropdown-menu>
<hlm-dropdown-menu-label>{{ _t()['actions'] }}</hlm-dropdown-menu-label>
<button hlmDropdownMenuItem (click)="copyPaymentId()">{{ _t()['copyId'] }}</button>
<hlm-dropdown-menu-separator />
<button hlmDropdownMenuItem>{{ _t()['viewCustomer'] }}</button>
<button hlmDropdownMenuItem>{{ _t()['viewPayment'] }}</button>
</hlm-dropdown-menu>
</ng-template>
`,
})
export class RtlActionDropdown {
public readonly row = input.required<Row<RtlDataTableFeatures, Payment>>();
private readonly _language = inject(TranslateService).language;
protected readonly _t = computed(() => TRANSLATIONS[this._language()].values);
copyPaymentId() {
navigator.clipboard.writeText(this.row().original.id);
}
}
@Component({
selector: 'spartan-data-table-rtl',
imports: [FlexRender, HlmButtonImports, HlmDropdownMenuImports, HlmInputImports, HlmTableImports, NgIcon],
providers: [provideIcons({ lucideChevronDown }), Directionality],
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
class: 'w-full',
'[dir]': '_dir()',
},
template: `
<div class="flex flex-col justify-between gap-4 py-4 sm:flex-row sm:items-center">
<input hlmInput class="w-full md:w-80" [placeholder]="_t()['filter']" (input)="_filterChanged($event)" />
<button hlmBtn variant="outline" align="end" [hlmDropdownMenuTrigger]="menu">
{{ _t()['columns'] }}
<ng-icon name="lucideChevronDown" class="ms-2" />
</button>
<ng-template #menu>
<hlm-dropdown-menu class="w-32">
@for (column of _hidableColumns; track column.id) {
<button hlmDropdownMenuCheckbox [checked]="column.getIsVisible()" (triggered)="column.toggleVisibility()">
<hlm-dropdown-menu-checkbox-indicator />
{{ _t()[column.id] }}
</button>
}
</hlm-dropdown-menu>
</ng-template>
</div>
<div class="overflow-hidden rounded-md border">
<div hlmTableContainer>
<table hlmTable>
<thead hlmTHead>
@for (headerGroup of _table.getHeaderGroups(); track headerGroup.id) {
<tr hlmTr>
@for (header of headerGroup.headers; track header.id) {
<th hlmTh [attr.colSpan]="header.colSpan">
@if (!header.isPlaceholder) {
<ng-container
*flexRender="header.column.columnDef.header; props: header.getContext(); let headerText"
>
<div [innerHTML]="headerText"></div>
</ng-container>
}
</th>
}
</tr>
}
</thead>
<tbody hlmTBody>
@for (row of _table.getRowModel().rows; track row.id) {
<tr hlmTr [attr.key]="row.id" [attr.data-state]="row.getIsSelected() && 'selected'">
@for (cell of row.getVisibleCells(); track $index) {
<td hlmTd>
<ng-container *flexRender="cell.column.columnDef.cell; props: cell.getContext(); let cell">
<div [innerHTML]="cell"></div>
</ng-container>
</td>
}
</tr>
} @empty {
<tr hlmTr>
<td hlmTd class="h-24 text-center" [attr.colspan]="_columns.length">{{ _t()['noResults'] }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<div class="flex flex-col justify-between py-4 sm:flex-row sm:items-center">
<div class="${hlmMuted}">
{{ _selectedLabel(_table.getSelectedRowModel().rows.length, _table.getRowCount()) }}
</div>
<div class="mt-2 flex gap-2 sm:mt-0">
<button
size="sm"
variant="outline"
hlmBtn
[disabled]="!_table.getCanPreviousPage()"
(click)="_table.previousPage()"
>
{{ _t()['previous'] }}
</button>
<button size="sm" variant="outline" hlmBtn [disabled]="!_table.getCanNextPage()" (click)="_table.nextPage()">
{{ _t()['next'] }}
</button>
</div>
</div>
`,
})
export class DataTableRtl {
private readonly _language = inject(TranslateService).language;
private readonly _translation = computed(() => TRANSLATIONS[this._language()]);
protected readonly _t = computed(() => this._translation().values);
protected readonly _dir = computed(() => this._translation().dir);
private readonly _locale = computed(() => this._translation().locale ?? 'en-US');
protected readonly _columns = columnHelper.columns([
columnHelper.display({
id: 'select',
header: () => RtlTableHeadSelection,
cell: () => RtlTableRowSelection,
enableHiding: false,
}),
columnHelper.accessor('status', {
id: 'status',
header: () => this._t()['status'],
cell: (info) => `<span>${this._t()[info.getValue<string>()]}</span>`,
}),
columnHelper.accessor('email', {
id: 'email',
header: () => RtlTableHeadSortButton,
cell: (info) => `<span class="lowercase" dir="ltr">${info.getValue<string>()}</span>`,
}),
columnHelper.accessor('amount', {
id: 'amount',
header: () => `<div class="text-end">${this._t()['amount']}</div>`,
cell: (info) => {
const amount = parseFloat(info.getValue<string>());
const formatted = new Intl.NumberFormat(this._locale(), {
style: 'currency',
currency: 'USD',
}).format(amount);
return `<div class="text-end font-medium">${formatted}</div>`;
},
}),
columnHelper.display({
id: 'actions',
cell: () => RtlActionDropdown,
enableHiding: false,
}),
]);
private readonly _columnFilters = signal<ColumnFiltersState>([]);
private readonly _sorting = signal<SortingState>([]);
private readonly _rowSelection = signal<RowSelectionState>({});
private readonly _columnVisibility = signal<ColumnVisibilityState>({});
protected readonly _table = injectTable(() => ({
features,
columns: this._columns,
data: PAYMENT_DATA,
onSortingChange: (updater) => {
isFunction(updater) ? this._sorting.update(updater) : this._sorting.set(updater);
},
onColumnFiltersChange: (updater) => {
isFunction(updater) ? this._columnFilters.update(updater) : this._columnFilters.set(updater);
},
onColumnVisibilityChange: (updater) => {
isFunction(updater) ? this._columnVisibility.update(updater) : this._columnVisibility.set(updater);
},
onRowSelectionChange: (updater) => {
isFunction(updater) ? this._rowSelection.update(updater) : this._rowSelection.set(updater);
},
state: {
sorting: this._sorting(),
columnFilters: this._columnFilters(),
columnVisibility: this._columnVisibility(),
rowSelection: this._rowSelection(),
},
}));
protected readonly _hidableColumns = this._table.getAllColumns().filter((column) => column.getCanHide());
private readonly _directionality = inject(Directionality);
constructor() {
effect(() => {
const dir = this._dir();
untracked(() => this._directionality.valueSignal.set(dir));
});
}
protected _filterChanged(event: Event) {
this._table.getColumn('email')?.setFilterValue((event.target as HTMLInputElement).value);
}
protected _selectedLabel(selected: number, total: number) {
const template = this._t()['selected'];
return template.replace('{selected}', `${selected}`).replace('{total}', `${total}`);
}
}
const PAYMENT_DATA: Payment[] = [
{
id: 'm5gr84i9',
amount: 316,
status: 'success',
email: 'ken99@yahoo.com',
},
{
id: '3u1reuv4',
amount: 242,
status: 'success',
email: 'Abe45@gmail.com',
},
{
id: 'derv1ws0',
amount: 837,
status: 'processing',
email: 'Monserrat44@gmail.com',
},
{
id: '5kma53ae',
amount: 874,
status: 'success',
email: 'Silas22@gmail.com',
},
{
id: 'bhqecj4p',
amount: 721,
status: 'failed',
email: 'carmella@hotmail.com',
},
];On This Page