data-display
datatable
Data Table
Sortable, filterable, paginated table — the flagship.
$
Component docs
ndui add datatable
Sort · search · paginate · delete
18 results| ID | Name ▲ | Price | Stock | |
|---|---|---|---|---|
| #101 | Artisan Mug | $24.00 | 142 | |
| #117 | Brass Candle Holder | $34.00 | 54 | |
| #108 | Cast Iron Skillet | $39.00 | 110 | |
| #113 | Ceramic Planter | $28.00 | 189 | |
| #102 | Ceramic Plate Set | $48.00 | 22 | |
| #105 | Chaise Lounge | $449.00 | Low · 5 | |
| #111 | Cheese Board | $58.00 | 24 | |
| #106 | Ergonomic Mesh Chair | $289.00 | 32 |
Showing 1–8 of 18
This demo is fully wired — typing searches live, column headers sort, pagination pages, and Delete removes rows. Every interaction is a GET/POST that returns the
_ProductsTable partial, morphed in by Alpine AJAX. State is in-memory for the demo; in a real app this would hit Tuxedo over SQLite.
Razor source — the view
<DataTable TItem="Product"
Items="@Model.PageItems" TotalCount="@Model.Total"
Page="@Model.Page" PageSize="@DatatableModel.PageSize"
SortColumn="@Model.Sort" SortDirection="@Model.Dir"
SearchQuery="@Model.Search"
TargetId="products_table"
FormAction="/dashboard/products">
<Columns>
<Column TItem="Product" Field="Id" Header="#" Width="4rem" />
<Column TItem="Product" Field="Name" Header="Name" Sortable="true" />
<Column TItem="Product" Field="Price" Header="Price" Sortable="true" Width="8rem">
<CellTemplate>@string.Format("{0:C}", context.Price)</CellTemplate>
</Column>
<Column TItem="Product" Field="Stock" Header="Stock" Sortable="true" Width="6rem">
<CellTemplate>
@if (context.Stock == 0) { <Badge Variant="BadgeVariant.Destructive">Out</Badge> }
else if (context.Stock < 10) { <Badge Variant="BadgeVariant.Warning">Low</Badge> }
else { <span>@context.Stock</span> }
</CellTemplate>
</Column>
</Columns>
</DataTable>
C# source — the PageModel
public sealed class IndexModel(ProductService products) : PageModel
{
public IReadOnlyList<Product> Items { get; private set; } = [];
public int Total { get; private set; }
[BindProperty(SupportsGet = true, Name = "q")] public string? Search { get; set; }
[BindProperty(SupportsGet = true, Name = "sort")] public string Sort { get; set; } = "name";
[BindProperty(SupportsGet = true, Name = "dir")] public string Dir { get; set; } = "asc";
[BindProperty(SupportsGet = true, Name = "page")] public int Page { get; set; } = 1;
public async Task<IActionResult> OnGetAsync(CancellationToken ct)
{
(Items, Total) = await products.ListAsync(Search, Sort, Dir, Page, PageSize: 20, ct);
return this.PageOrPartial("_ProductsTable", this);
}
public async Task<IActionResult> OnPostDeleteAsync(int id, CancellationToken ct)
{
if (!await products.DeleteAsync(id, ct)) return NotFound();
this.AddToast("Product deleted.", ToastLevel.Success);
(Items, Total) = await products.ListAsync(Search, Sort, Dir, Page, 20, ct);
return this.PageOrPartial("_ProductsTable", this);
}
}
Three pieces coordinate: [BindProperty(SupportsGet = true)] rehydrates query params into typed properties; PageOrPartial chooses full page vs. fragment based on the X-Alpine-Request header; AddToast plus the registered sync block show the delete confirmation with no per-form wiring.
Full walkthrough: Docs → DataTable deep-dive.