ee8797c142
Co-authored-by: Copilot <copilot@github.com>
214 lines
7.7 KiB
Markdown
214 lines
7.7 KiB
Markdown
# Toast
|
||
|
||
A transient notification that appears in the bottom-right corner (or wherever `ToastViewport` is placed), auto-dismisses after a configurable duration, and can be dismissed manually.
|
||
|
||
Toasts are triggered **client-side** via `window.showToast(...)` from JavaScript — they are not server-rendered components like most others. The `Toast` component class produces the initial toast markup for use as a static template or in the ToastViewport; in practice most toasts are created dynamically by the JS API.
|
||
|
||
---
|
||
|
||
## HTML structure (dynamically created by JS)
|
||
|
||
```
|
||
div.toast[role=alert, aria-live=polite, data-variant]
|
||
div.flex.items-start.gap-3
|
||
div.flex-1
|
||
p.font-medium.text-sm ← title
|
||
p.text-sm.text-muted-foreground ← description (omitted when empty)
|
||
button.ml-auto[aria-label=Dismiss] ← × close button
|
||
svg (×)
|
||
```
|
||
|
||
The outer `div.toast` is appended to the `ToastViewport` container by JS and removed after `duration` ms.
|
||
|
||
---
|
||
|
||
## CSS mechanics
|
||
|
||
| Class | Effect |
|
||
|---|---|
|
||
| `toast` | Defined in `input.css`: `w-80 rounded-lg border bg-background p-4 shadow-lg pointer-events-auto` |
|
||
| `toast-enter` / `toast-exit` | CSS keyframe animation classes applied by JS for slide-in/fade-out |
|
||
| `data-variant="default"` | Border `border-border` |
|
||
| `data-variant="destructive"` | Border `border-destructive`, title `text-destructive` |
|
||
| `data-variant="success"` | Border `border-green-500` |
|
||
|
||
---
|
||
|
||
## JavaScript (`showToast` in `components.js`)
|
||
|
||
```js
|
||
window.showToast({
|
||
title: "Operation complete", // required
|
||
description: "All items saved.", // optional
|
||
variant: "success", // "default" | "destructive" | "success"
|
||
duration: 4000 // milliseconds before auto-dismiss
|
||
});
|
||
```
|
||
|
||
**Implementation steps:**
|
||
|
||
1. Build the toast `div` element with the classes and markup described above
|
||
2. Apply `toast-enter` class → CSS slide-in animation plays
|
||
3. Append to the `ToastViewport` (`#toast-viewport` by default, or the first `.toast-viewport` found)
|
||
4. After `duration` ms, apply `toast-exit` class → CSS fade-out animation plays
|
||
5. After fade-out completes, remove the element from the DOM
|
||
6. Dismiss button click runs the same fade-out + remove cycle immediately
|
||
|
||
---
|
||
|
||
## Constructor signature
|
||
|
||
```csharp
|
||
public Toast(
|
||
string title,
|
||
string description = "",
|
||
string variant = "default")
|
||
```
|
||
|
||
The constructor builds a static initial toast element. Most use-cases call `window.showToast(...)` from JS instead.
|
||
|
||
| Parameter | Description |
|
||
|---|---|
|
||
| `title` | Required notification heading |
|
||
| `description` | Optional body text |
|
||
| `variant` | `"default"` / `"destructive"` / `"success"` |
|
||
|
||
---
|
||
|
||
## Usage examples
|
||
|
||
### Trigger from JavaScript after an HTMX event
|
||
|
||
```js
|
||
document.body.addEventListener('htmx:afterRequest', function (e) {
|
||
if (e.detail.successful) {
|
||
window.showToast({ title: 'Saved', variant: 'success', duration: 3000 });
|
||
} else {
|
||
window.showToast({ title: 'Error', description: 'Could not save.', variant: 'destructive' });
|
||
}
|
||
});
|
||
```
|
||
|
||
### Trigger from a server response header
|
||
|
||
Add a response header `HX-Trigger` in your handler:
|
||
|
||
```csharp
|
||
ctx.Response.Headers.Append("HX-Trigger",
|
||
"""{"showToast":{"title":"Saved!","variant":"success","duration":3000}}""");
|
||
```
|
||
|
||
Client-side listener:
|
||
|
||
```js
|
||
document.body.addEventListener('showToast', function (e) {
|
||
window.showToast(e.detail);
|
||
});
|
||
```
|
||
|
||
### Server-rendered initial toast (rare)
|
||
|
||
```csharp
|
||
// Used as a slot inside a page that always shows a greeting on first load:
|
||
protected override void RenderWelcomeToast(HtmxRenderContext ctx)
|
||
=> new Toast("Welcome back!", "Your dashboard is ready.", "success").Render(ctx.Next());
|
||
```
|
||
|
||
---
|
||
|
||
## Tips and tricks
|
||
|
||
- Always place a single `ToastViewport` in your main layout so toasts have a container to render into. See [ToastViewport.md](ToastViewport.md).
|
||
- Use the `HX-Trigger` header pattern to trigger toasts from HTMX responses — it keeps toast logic on the server without requiring extra HTMX endpoints.
|
||
- `duration: 0` means the toast never auto-dismisses — the user must click the × button.
|
||
- Multiple toasts stack upward in the viewport (new ones appear above older ones) due to `flex-col-reverse` in `ToastViewport`.
|
||
- For progress toasts that update as a background job runs, call `showToast` once and then use a reference to the element to update the description text.
|
||
- For progress toasts that update as a background job runs, call `showToast` once and then use a reference to the element to update the description text.
|
||
|
||
---
|
||
|
||
## Complete page example
|
||
|
||
**`Templates/ContactFormPage.htmx`**
|
||
```html
|
||
<div class="max-w-lg mx-auto py-10">
|
||
<h1 class="text-2xl font-bold mb-6">Contact us</h1>
|
||
<form hx-post="/contact"
|
||
hx-target="this"
|
||
hx-swap="outerHTML">
|
||
$$AntiforgeryToken$$
|
||
<div class="space-y-4 mb-6">
|
||
$$NameInput$$
|
||
$$EmailInput$$
|
||
$$MessageArea$$
|
||
</div>
|
||
$$SubmitBtn$$
|
||
</form>
|
||
</div>
|
||
```
|
||
|
||
**`Templates/ContactFormPage.htmx.cs`**
|
||
```csharp
|
||
namespace Htmx.ApiDemo.Templates;
|
||
|
||
public sealed class ContactFormPage : ContactFormPageBase
|
||
{
|
||
private readonly IHtmxComponent _name;
|
||
private readonly IHtmxComponent _email;
|
||
private readonly IHtmxComponent _message;
|
||
private readonly IHtmxComponent _submit;
|
||
private readonly byte[] _afToken;
|
||
|
||
public ContactFormPage(IAntiforgery af, HttpContext ctx)
|
||
{
|
||
var tokens = af.GetAndStoreTokens(ctx);
|
||
_afToken = $"""<input type="hidden" name="{tokens.FormFieldName}" value="{tokens.RequestToken}">""".ToUtf8Bytes();
|
||
|
||
_name = new Components.Input(id: "name", name: "name", label: "Name", placeholder: "Jane Smith");
|
||
_email = new Components.Input(id: "email", name: "email", label: "Email", placeholder: "jane@example.com", type: "email");
|
||
_message = new Components.Textarea(id: "message", name: "message", label: "Message", rows: 4);
|
||
_submit = new Components.Button("Send message", type: "submit");
|
||
}
|
||
|
||
protected override void RenderAntiforgeryToken(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_afToken);
|
||
protected override void RenderNameInput(HtmxRenderContext ctx) => _name.Render(ctx.Next());
|
||
protected override void RenderEmailInput(HtmxRenderContext ctx) => _email.Render(ctx.Next());
|
||
protected override void RenderMessageArea(HtmxRenderContext ctx) => _message.Render(ctx.Next());
|
||
protected override void RenderSubmitBtn(HtmxRenderContext ctx) => _submit.Render(ctx.Next());
|
||
}
|
||
```
|
||
|
||
**POST handler — triggers a toast via `HX-Trigger`**
|
||
```csharp
|
||
[Handler]
|
||
[MapPost("/contact")]
|
||
public static partial class PostContactHandler
|
||
{
|
||
public record Command(
|
||
[property: FromForm] string Name,
|
||
[property: FromForm] string Email,
|
||
[property: FromForm] string Message);
|
||
|
||
private static Task<IResult> HandleAsync(
|
||
[AsParameters] Command cmd, HttpContext ctx, IAntiforgery af, CancellationToken ct)
|
||
{
|
||
// Persist / send message…
|
||
|
||
// Re-render empty form so user can send another message
|
||
var buf = new System.Buffers.ArrayBufferWriter<byte>();
|
||
new ContactFormPage(af, ctx).Render(new HtmxRenderContext(buf));
|
||
|
||
ctx.Response.Headers["HX-Trigger"] = """{"showToast":{"title":"Message sent!","description":"We'll get back to you soon."}}""";
|
||
return Task.FromResult(Results.Content(
|
||
System.Text.Encoding.UTF8.GetString(buf.WrittenSpan), "text/html"));
|
||
}
|
||
}
|
||
```
|
||
|
||
> **Tip**: The `HX-Trigger` header fires the `showToast` custom event that the `<toast-viewport>` element listens for (see `ToastViewport`).
|
||
|
||
**`AppJsonSerializerContext.cs`**
|
||
```csharp
|
||
[JsonSerializable(typeof(PostContactHandler.Command), TypeInfoPropertyName = "ContactCommand")]
|
||
```
|