Files
Htmx/docs/Components/Table.md
T
2026-05-04 19:57:48 +05:00

201 lines
5.4 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 data table with a header row, optional caption, optional footer row, and one or more data rows. All data is plain strings.
---
## HTML structure
```
div.overflow-auto.rounded-md.border.border-border
table.w-full.text-sm.caption-bottom
caption.mt-4.text-sm.text-muted-foreground ← omitted when empty
{caption}
thead
tr.border-b.bg-muted/50
th.h-12.px-4.text-left.font-medium ← one per header
{header}
tbody
tr.border-b.hover:bg-muted/40 ← one per row; last row has no border
td.p-4 ← one per cell; raw HTML
{cell}
tfoot ← omitted when empty
tr
td[colspan=N].p-4.text-muted-foreground
{footer}
```
---
## CSS mechanics
| Class | Effect |
|---|---|
| `overflow-auto` on wrapper | Horizontal scroll on small screens |
| `bg-muted/50` on header | Slightly tinted header row |
| `hover:bg-muted/40` on data rows | Subtle hover highlight |
| `border-b` on rows | Row separator lines |
| `caption-bottom` | Caption appears below the table |
---
## Constructor signature
```csharp
public Table(
IEnumerable<string> headers,
IEnumerable<IEnumerable<string>> rows,
string caption = "",
string footer = "")
```
| Parameter | Description |
|---|---|
| `headers` | Column heading strings |
| `rows` | Each inner `IEnumerable<string>` is one row; cells are raw HTML |
| `caption` | Optional caption below the table |
| `footer` | Optional footer cell (spans all columns) |
---
## Usage examples
### Basic data table
```csharp
new Table(
headers: new[] { "Name", "Email", "Role" },
rows: users.Select(u => new[] { u.DisplayName ?? "", u.Email, u.Role }))
```
### With caption and footer
```csharp
new Table(
headers: new[] { "Product", "Price", "Stock" },
rows: products.Select(p => new[]
{
p.Name,
$"${p.Price:F2}",
p.Stock.ToString()
}),
caption: $"Showing {products.Count} products",
footer: "Prices include VAT")
```
### Cells with HTML content (e.g. badges)
```csharp
// Pre-render a Badge to HTML string
string ActiveBadge()
{
var buf = new System.Buffers.ArrayBufferWriter<byte>();
new Badge("Active").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 ? ActiveBadge() : ""
}))
```
### With action buttons per row
```csharp
string EditBtn(string id) => $"""
<button hx-get="/users/{id}/edit" hx-target="#modal" class="text-sm underline">Edit</button>
""";
new Table(
headers: new[] { "Name", "Actions" },
rows: users.Select(u => new[]
{
System.Web.HttpUtility.HtmlEncode(u.DisplayName ?? ""),
EditBtn(u.Id!)
}))
```
---
## 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");
}
}
```