ee8797c142
Co-authored-by: Copilot <copilot@github.com>
155 lines
4.8 KiB
Markdown
155 lines
4.8 KiB
Markdown
# ToastViewport
|
|
|
|
The fixed container that holds all `Toast` notifications. Place exactly one `ToastViewport` in your main layout (e.g. `MainLayout.htmx`). The viewport is invisible when empty and stacks toasts upward as they are added.
|
|
|
|
---
|
|
|
|
## HTML structure
|
|
|
|
```
|
|
div[id={id}].toast-viewport.fixed.bottom-4.right-4.z-50.flex.flex-col-reverse.gap-2.w-80.pointer-events-none
|
|
```
|
|
|
|
---
|
|
|
|
## CSS mechanics
|
|
|
|
| Class | Effect |
|
|
|---|---|
|
|
| `fixed bottom-4 right-4` | Anchored to the bottom-right corner of the viewport |
|
|
| `z-50` | Floats above all other content including dialogs and dropdowns |
|
|
| `flex flex-col-reverse gap-2` | New toasts appear on top; older ones push downward |
|
|
| `w-80` | Matches the default toast width |
|
|
| `pointer-events-none` | The container itself doesn't capture clicks — toasts set `pointer-events-auto` individually |
|
|
|
|
---
|
|
|
|
## Constructor signature
|
|
|
|
```csharp
|
|
public ToastViewport(string id = "toast-viewport")
|
|
```
|
|
|
|
| Parameter | Description |
|
|
|---|---|
|
|
| `id` | Element id (default: `"toast-viewport"`). `components.js` queries `#toast-viewport` by default — only change this if you also update the JS lookup. |
|
|
|
|
---
|
|
|
|
## Usage examples
|
|
|
|
### Place in MainLayout
|
|
|
|
```html
|
|
<!-- MainLayout.htmx -->
|
|
<body class="...">
|
|
<main>$$Body$$</main>
|
|
$$ToastViewport$$
|
|
</body>
|
|
```
|
|
|
|
```csharp
|
|
// MainLayout.htmx.cs
|
|
public IHtmxComponent ToastViewport { get; } = new ToastViewport();
|
|
|
|
protected override void RenderToastViewport(HtmxRenderContext ctx)
|
|
=> ToastViewport.Render(ctx.Next());
|
|
```
|
|
|
|
### Custom id (advanced)
|
|
|
|
```csharp
|
|
new ToastViewport(id: "notifications-container")
|
|
```
|
|
|
|
Then update the JS lookup:
|
|
|
|
```js
|
|
// In components.js or a custom script:
|
|
const viewport = document.getElementById('notifications-container');
|
|
```
|
|
|
|
### Custom position (bottom-left)
|
|
|
|
The position is set by Tailwind classes on the rendered element. To change position, subclass the component or pass `extraClasses` if supported, or override the `toast-viewport` class in your `input.css`:
|
|
|
|
```css
|
|
.toast-viewport {
|
|
bottom: 1rem;
|
|
left: 1rem;
|
|
right: auto;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Tips and tricks
|
|
|
|
- Place `ToastViewport` once in the outermost layout — not inside HTMX swap targets, since HTMX replaces the target's contents and would remove the viewport.
|
|
- The default id `"toast-viewport"` is hard-coded in `components.js` for the `showToast` lookup. If you rename it, update the JS too.
|
|
- `ToastViewport` renders as an empty `div` — it has no visual presence until a toast is appended to it.
|
|
- Multiple viewports on the same page are valid for different toast regions (e.g. top-right and bottom-right), but `showToast` will target whichever viewport id it is configured for.
|
|
- Multiple viewports on the same page are valid for different toast regions (e.g. top-right and bottom-right), but `showToast` will target whichever viewport id it is configured for.
|
|
|
|
---
|
|
|
|
## Complete page example
|
|
|
|
`ToastViewport` is a layout-level concern — it lives in `MainLayout`, not in individual page templates. The example below shows the full integration pattern.
|
|
|
|
**`Templates/MainLayout.htmx`** (excerpt)
|
|
```html
|
|
<body class="min-h-screen bg-background text-foreground">
|
|
$$NavBar$$
|
|
<main class="container mx-auto px-4 py-8">
|
|
$$Content$$
|
|
</main>
|
|
$$ToastViewport$$
|
|
$$Scripts$$
|
|
</body>
|
|
```
|
|
|
|
**`Templates/MainLayout.htmx.cs`** (excerpt)
|
|
```csharp
|
|
namespace Htmx.ApiDemo.Templates;
|
|
|
|
public sealed class MainLayout : MainLayoutBase
|
|
{
|
|
private readonly IHtmxComponent _nav;
|
|
private readonly IHtmxComponent _content;
|
|
private readonly IHtmxComponent _viewport;
|
|
|
|
public MainLayout(IHtmxComponent content, IHtmxComponent nav)
|
|
{
|
|
_nav = nav;
|
|
_content = content;
|
|
|
|
// Place one viewport at bottom-right for all pages
|
|
_viewport = new Components.ToastViewport(
|
|
id: "toast-viewport",
|
|
position: "bottom-right");
|
|
}
|
|
|
|
protected override void RenderNavBar(HtmxRenderContext ctx) => _nav.Render(ctx.Next());
|
|
protected override void RenderContent(HtmxRenderContext ctx) => _content.Render(ctx.Next());
|
|
protected override void RenderToastViewport(HtmxRenderContext ctx) => _viewport.Render(ctx.Next());
|
|
}
|
|
```
|
|
|
|
**Triggering a toast from any handler**
|
|
```csharp
|
|
// Any POST handler can fire a toast without changing the ToastViewport markup.
|
|
ctx.Response.Headers["HX-Trigger"] =
|
|
"""{"showToast":{"title":"Saved!","description":"Your changes have been persisted."}}""";
|
|
```
|
|
|
|
**Two-viewport layout** (top-right errors + bottom-right successes)
|
|
```csharp
|
|
_errorViewport = new Components.ToastViewport(id: "error-viewport", position: "top-right");
|
|
_successViewport = new Components.ToastViewport(id: "success-viewport", position: "bottom-right");
|
|
|
|
// Error toast
|
|
ctx.Response.Headers["HX-Trigger"] =
|
|
"""{"showToast":{"viewportId":"error-viewport","title":"Something went wrong."}}""";
|
|
```
|