f6ae86617c
Co-authored-by: Copilot <copilot@github.com>
212 lines
6.5 KiB
Markdown
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);
|
|
```
|