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

253 lines
8.2 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.
# Dialog
A modal dialog using the native HTML `<dialog>` element. Content is organized into optional title, description, body, and footer sections. Open/close is handled by client-side JS via delegated click events on `data-dialog-open` and `data-dialog-close` attributes.
---
## HTML structure
```
dialog[id, class=...]
div.dialog-panel.relative.bg-background.p-6.rounded-lg.shadow-xl.w-full.max-w-md...
button.absolute.top-4.right-4[data-dialog-close={id}] ← × close button
h2.text-lg.font-semibold ← title (omitted when empty)
p.text-sm.text-muted-foreground.mt-1 ← description (omitted when empty)
div.mt-4 ← body content
{content}
div.mt-6.flex.justify-end.gap-2 ← footer (omitted when empty)
{footer}
```
---
## CSS mechanics
| Class | Effect |
|---|---|
| `dialog::backdrop` (in `input.css`) | Semi-transparent black overlay behind the dialog |
| `animate-in fade-in-0 zoom-in-95` | CSS entry animation when dialog opens |
| `max-w-md w-full` | Responsive: full width on small screens, capped at `md` |
| `overflow-y-auto max-h-[90vh]` | Scrollable body for tall content |
---
## JavaScript (delegated clicks in `components.js`)
Set up once on `document` and works for all dialogs on the page, including those HTMX-swapped in.
### Open
Any element with `data-dialog-open="myDialogId"` calls `document.getElementById('myDialogId').showModal()`.
### Close
Any element with `data-dialog-close="myDialogId"` calls `document.getElementById('myDialogId').close()`.
The `×` close button inside the dialog panel already has `data-dialog-close` set to the dialog's id.
Clicking the `::backdrop` (outside the panel) also closes the dialog — the click handler checks whether the click target is the `<dialog>` element itself.
---
## Constructor signature
```csharp
public Dialog(
string id,
string content,
string title = "",
string description = "",
string footer = "")
```
| Parameter | Description |
|---|---|
| `id` | Element id — must be unique per page; also used by `data-dialog-open` |
| `content` | Raw HTML for the dialog body |
| `title` | Optional heading at the top of the panel |
| `description` | Optional subheading below the title |
| `footer` | Optional raw HTML for the bottom button row |
---
## Usage examples
### Simple information dialog
```csharp
// In the page component:
Dialog = new Dialog(
id: "about-dialog",
title: "About BeepBoop",
description: "A fast AOT-safe HTMX framework.",
content: "<p>Version 1.0 — built with ❤️ and .NET 10.</p>",
footer: """<button data-dialog-close="about-dialog" class="...">Close</button>""");
```
Trigger button anywhere on the page:
```html
<button data-dialog-open="about-dialog" class="...">About</button>
```
### Confirmation dialog
```csharp
new Dialog(
id: "confirm-delete",
title: "Delete item",
content: "<p>This action cannot be undone.</p>",
footer: """
<button data-dialog-close="confirm-delete" class="...">Cancel</button>
<button hx-delete="/items/42" hx-confirm="" data-dialog-close="confirm-delete"
class="bg-destructive text-destructive-foreground ...">
Delete
</button>
""")
```
### HTMX-powered content reload
```csharp
new Dialog(
id: "user-detail",
content: """<div hx-get="/users/42/detail" hx-trigger="revealed"></div>""")
```
The `revealed` trigger fires when the dialog becomes visible, loading content on demand.
### Embedding inside a page slot
```html
<!-- MyPage.htmx -->
$$DeleteDialog$$
<button data-dialog-open="confirm-delete" class="...">Delete</button>
```
```csharp
public IHtmxComponent DeleteDialog { get; }
public MyPage()
{
DeleteDialog = new Dialog(
id: "confirm-delete",
title: "Confirm deletion",
content: "<p>Are you sure?</p>",
footer: """<button data-dialog-close="confirm-delete">Cancel</button>""");
}
protected override void RenderDeleteDialog(HtmxRenderContext ctx)
=> DeleteDialog.Render(ctx.Next());
```
---
## Tips and tricks
- The `id` is used both on the `<dialog>` element and in `data-dialog-open`/`data-dialog-close` — keep it unique per page.
- The `×` close button is always rendered; `data-dialog-close` on footer buttons is optional but improves UX.
- Use the native `<dialog>` `close` event for any cleanup needed after dismissal: `document.getElementById('id').addEventListener('close', fn)`.
- Dialog content, title, description, and footer are raw HTML — HTML-encode user-supplied values.
- For dialogs that load content via HTMX, place the HTMX attributes on a child div inside `content` rather than on the `<dialog>` element itself to avoid interfering with the dialog open/close lifecycle.
- For dialogs that load content via HTMX, place the HTMX attributes on a child div inside `content` rather than on the `<dialog>` element itself to avoid interfering with the dialog open/close lifecycle.
---
## Complete page example
**`Templates/ItemsPage.htmx`**
```html
<div class="max-w-3xl mx-auto py-10">
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold">Items</h1>
</div>
$$ItemsTable$$
$$DeleteDialog$$
</div>
```
**`Templates/ItemsPage.htmx.cs`**
```csharp
namespace Htmx.ApiDemo.Templates;
public sealed class ItemsPage : ItemsPageBase
{
private readonly IHtmxComponent _table;
private readonly IHtmxComponent _deleteDialog;
public ItemsPage(IEnumerable<Item> items, string? deleteTargetId = null)
{
// Build table with a per-row Delete button that opens the dialog
_table = new Components.Table(
headers: new[] { "Name", "Created", "Actions" },
rows: items.Select(item => new[]
{
System.Net.WebUtility.HtmlEncode(item.Name),
item.CreatedAt.ToShortDateString(),
$"""<button data-dialog-open="confirm-delete"
hx-on:click="document.getElementById('delete-id').value='{item.Id}'"
class="text-sm text-destructive underline">Delete</button>""",
}));
_deleteDialog = new Components.Dialog(
id: "confirm-delete",
title: "Delete item",
content: """
<p class="text-sm">This action cannot be undone.</p>
<input type="hidden" id="delete-id" name="itemId">
""",
footer: """
<button data-dialog-close="confirm-delete"
class="inline-flex h-9 rounded-md border border-input px-4 text-sm mr-2">
Cancel
</button>
<button hx-delete="/items"
hx-include="#delete-id"
hx-target="closest tr"
hx-swap="outerHTML"
data-dialog-close="confirm-delete"
class="inline-flex h-9 rounded-md bg-destructive text-destructive-foreground px-4 text-sm">
Delete
</button>
""");
}
protected override void RenderItemsTable(HtmxRenderContext ctx) => _table.Render(ctx.Next());
protected override void RenderDeleteDialog(HtmxRenderContext ctx) => _deleteDialog.Render(ctx.Next());
}
```
**GET + DELETE handlers**
```csharp
[Handler]
[MapGet("/items")]
public static partial class GetItemsHandler
{
public record Query();
private static Task<IResult> HandleAsync(
Query _, HttpContext ctx, CancellationToken ct)
{
var items = new[]
{
new Item("1", "Widget A", DateTime.Today.AddDays(-10)),
new Item("2", "Widget B", DateTime.Today.AddDays(-5)),
};
return ctx.WriteHtmxPage(new ItemsPage(items), title: "Items");
}
}
[Handler]
[MapDelete("/items")]
public static partial class DeleteItemHandler
{
public record Command([property: FromForm] string ItemId);
private static IResult HandleAsync([AsParameters] Command cmd, CancellationToken ct)
{
// Delete item from database…
return Results.Ok(); // HTMX swaps the row out with outerHTML
}
}
public record Item(string Id, string Name, DateTime CreatedAt);
```