ee8797c142
Co-authored-by: Copilot <copilot@github.com>
5.4 KiB
5.4 KiB
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
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
new Table(
headers: new[] { "Name", "Email", "Role" },
rows: users.Select(u => new[] { u.DisplayName ?? "", u.Email, u.Role }))
With caption and footer
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)
// 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
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)orSystem.Net.WebUtility.HtmlEncode(value)for any untrusted data. - Pair with
Paginationbelow the table for large datasets. - For sortable columns, replace header strings with anchor tags containing HTMX sort-request attributes.
- The
footerstring spans all columns — use it for totals, notes, or a "load more" link. - The
footerstring spans all columns — use it for totals, notes, or a "load more" link.
Complete page example
Templates/AdminUsersPage.htmx
<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
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
[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");
}
}