N
data-display datatable

Data Table

Sortable, filterable, paginated table — the flagship.

$ ndui add datatable
Component docs

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.