Files
Htmx/docs/Components/Table.md
T
2026-05-05 23:55:26 +05:00

182 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Table
A styled HTML table with a header row, data rows, and optional caption and footer. Use it when you have a list of items with multiple columns — user lists, order history, product inventories.
---
## Quick example
```csharp
new Table(
headers: new[] { "Name", "Email", "Role" },
rows: users.Select(u => new[]
{
System.Web.HttpUtility.HtmlEncode(u.DisplayName ?? ""),
System.Web.HttpUtility.HtmlEncode(u.Email),
u.Role
}))
```
---
## All the options
```csharp
public Table(
IEnumerable<string> headers,
IEnumerable<IEnumerable<string>> rows,
string caption = "",
string footer = "")
```
| Parameter | What it does |
|---|---|
| `headers` | Column heading strings. |
| `rows` | Each inner collection is one table row. Each string in it is a cell. Cells are raw HTML. |
| `caption` | Optional summary text displayed below the table. |
| `footer` | Optional footer text that spans all columns. |
> **HTML safety:** Cell values are inserted as raw HTML. Always use `System.Web.HttpUtility.HtmlEncode()` on any user-supplied strings before passing them in.
---
## Real-world examples
### Table with a count caption and footer note
```csharp
new Table(
headers: new[] { "Product", "Price", "Stock" },
rows: products.Select(p => new[]
{
System.Web.HttpUtility.HtmlEncode(p.Name),
$"${p.Price:F2}",
p.Stock.ToString()
}),
caption: $"Showing {products.Count} products",
footer: "Prices include VAT")
```
### Status column with a Badge
Pre-render the badge to an HTML string and embed it in the cell:
```csharp
string RenderBadge(string label, string variant = "default")
{
var buf = new System.Buffers.ArrayBufferWriter<byte>();
new Badge(label, variant).Render(new HtmxRenderContext(buf));
return System.Text.Encoding.UTF8.GetString(buf.WrittenSpan);
}
new Table(
headers: new[] { "Name", "Status" },
rows: users.Select(u => new[]
{
System.Web.HttpUtility.HtmlEncode(u.DisplayName ?? ""),
u.IsActive ? RenderBadge("Active", "default") : RenderBadge("Inactive", "secondary")
}))
```
### Row actions with HTMX edit button
```csharp
string EditLink(string id) =>
$"""<button hx-get="/users/{id}/edit" hx-target="#modal" class="text-sm underline">Edit</button>""";
new Table(
headers: new[] { "Name", "" },
rows: users.Select(u => new[]
{
System.Web.HttpUtility.HtmlEncode(u.DisplayName ?? ""),
EditLink(u.Id!)
}))
```
---
## How it works
Table wraps a standard `<table>` in an `overflow-auto` container so it scrolls horizontally on small screens. Header cells use `<th>` and data cells use `<td>`. The `caption` is rendered inside a `<caption>` element below the table; the `footer` spans all columns in a `<tfoot>` row.
```
---
## Tips and tricks
- Cells are raw HTML — HTML-encode any user-supplied string values before inserting them to prevent XSS.
- Use `System.Web.HttpUtility.HtmlEncode(value)` or `System.Net.WebUtility.HtmlEncode(value)` for any untrusted data.
- Pair with `Pagination` below the table for large datasets.
- For sortable columns, replace header strings with anchor tags containing HTMX sort-request attributes.
- The `footer` string spans all columns — use it for totals, notes, or a "load more" link.
- The `footer` string spans all columns — use it for totals, notes, or a "load more" link.
---
## Complete page example
**`Templates/AdminUsersPage.htmx`**
```html
<div class="max-w-5xl mx-auto py-10">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Users</h1>
$$InviteBtn$$
</div>
$$UsersTable$$
</div>
```
**`Templates/AdminUsersPage.htmx.cs`**
```csharp
namespace Htmx.ApiDemo.Templates;
public sealed class AdminUsersPage : AdminUsersPageBase
{
private readonly IHtmxComponent _table;
private readonly IHtmxComponent _invite;
public AdminUsersPage(IEnumerable<ApplicationUser> users, int total)
{
_invite = new Components.Button(
"Invite user",
variant: "default",
extraAttributes: """hx-get="/admin/users/invite" hx-target="#modal" hx-swap="innerHTML" """);
var rows = users.Select(u => new[]
{
System.Net.WebUtility.HtmlEncode(u.DisplayName ?? ""),
System.Net.WebUtility.HtmlEncode(u.Email),
u.CreatedAt.ToString("yyyy-MM-dd"),
$"""<button class="text-destructive text-xs" hx-delete="/admin/users/{u.Id}" hx-confirm="Delete this user?">Delete</button>""",
});
_table = new Components.Table(
caption: "All registered accounts",
headers: new[] { "Name", "Email", "Joined", "Actions" },
rows: rows,
footer: $"{total} users total");
}
protected override void RenderInviteBtn(HtmxRenderContext ctx) => _invite.Render(ctx.Next());
protected override void RenderUsersTable(HtmxRenderContext ctx) => _table.Render(ctx.Next());
}
```
**GET handler**
```csharp
[Handler]
[MapGet("/admin/users")]
public static partial class GetAdminUsersHandler
{
public record Query();
private static async Task<IResult> HandleAsync(
Query _, HttpContext ctx, MongoDbService db, CancellationToken ct)
{
var users = await db.GetAllUsersAsync(ct);
var list = users.ToList();
return await ctx.WriteHtmxPage(
new AdminUsersPage(list, list.Count), title: "Admin Users");
}
}
```