ee8797c142
Co-authored-by: Copilot <copilot@github.com>
201 lines
5.4 KiB
Markdown
201 lines
5.4 KiB
Markdown
# 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");
|
||
}
|
||
}
|
||
```
|