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

212 lines
6.5 KiB
Markdown

# Dialog
A modal pop-up window that appears on top of the page. Think of it like a small piece of paper sliding onto the desk — the rest of the page dims and you have to deal with the dialog before you can go back to work.
Opening and closing is handled entirely with `data-dialog-open` and `data-dialog-close` HTML attributes — no custom JavaScript needed.
---
## Quick example
```csharp
new Dialog(
id: "about-dialog",
title: "About this app",
content: "<p>Version 1.0 — built with .NET 10.</p>",
footer: """<button data-dialog-close="about-dialog">Close</button>""")
```
Then somewhere on the page, add a trigger:
```html
<button data-dialog-open="about-dialog">About</button>
```
That's it. No JavaScript needed in your templates.
---
## All the options
```csharp
public Dialog(
string id,
string content,
string title = "",
string description = "",
string footer = "")
```
| Parameter | What it does |
|---|---|
| `id` | A unique identifier. Used both on the `<dialog>` element and in `data-dialog-open`. |
| `content` | The body of the dialog. Raw HTML. |
| `title` | Optional bold heading at the top of the dialog panel. |
| `description` | Optional smaller text below the title. |
| `footer` | Optional button row at the bottom. Raw HTML. |
The title, description, and footer sections are omitted entirely from the HTML when not provided.
---
## Real-world examples
### Confirmation before a destructive action
Place the dialog in the page template, then trigger it from a button:
```html
<!-- Templates/ItemsPage.htmx -->
$$DeleteDialog$$
<!-- ... rest of page ... -->
<button data-dialog-open="confirm-delete">Delete item</button>
```
```csharp
// Templates/ItemsPage.htmx.cs
_deleteDialog = new Dialog(
id: "confirm-delete",
title: "Delete item",
content: "<p>This action cannot be undone.</p>",
footer: """
<button data-dialog-close="confirm-delete">Cancel</button>
<button hx-delete="/items/42" data-dialog-close="confirm-delete"
class="bg-destructive text-destructive-foreground ...">
Delete
</button>
""");
```
### Dialog that loads content on demand
Use HTMX's `revealed` trigger to load the dialog body only when it opens:
```csharp
new Dialog(
id: "user-detail",
title: "User details",
content: """<div hx-get="/users/42/detail" hx-trigger="revealed"></div>""")
```
### Closing from outside the dialog
Any element anywhere on the page can close a dialog by setting `data-dialog-close`:
```html
<button data-dialog-close="confirm-delete">Never mind</button>
```
---
## How it works
Dialog uses the native HTML `<dialog>` element with `showModal()`. A backdrop (the dark overlay) comes from the browser's built-in `::backdrop` pseudo-element, styled in `input.css`.
JavaScript in `components.js` listens for clicks anywhere on the page. If the clicked element has `data-dialog-open`, it calls `showModal()` on the matching dialog. If it has `data-dialog-close`, it calls `close()`. Clicking outside the dialog panel (on the backdrop) also closes it.
Because the listener is on `document`, dialogs that are HTMX-swapped in work automatically without any re-initialisation.
All `content` and `footer` strings are raw HTML — HTML-encode any user-supplied values before passing them in.
---
## 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);
```