N

Toast after action (sync block)

Any handler calls this.AddToast(...). The global _ToastContainer sync block refreshes on every htmx response — no per-form wiring needed.

#When to use

Every write operation. Save, delete, invite, export — if the user clicked a button, they deserve feedback.

#Program.cs

Register the sync block at startup (this line lands automatically when you ndui add toast):

csharp
builder.Services.AddHtmx(opts =>
{
    opts.RegisterSyncBlock("_ToastContainer");
});

#Layout

Drop the container once in _Layout.cshtml:

html
<partial name="_ToastContainer" />

The partial renders a single OOB element:

cshtml
@{
    var payload = Json.Serialize(Context.DrainToasts());
}
<div id="_ToastContainer" hx-swap-oob="true" data-toasts='@Html.Raw(payload)' hidden></div>

A small Alpine listener on htmx:afterSwap reads data-toasts and dispatches each one through window.toast.*.

#Handler

Raise from any PageModel — the toast flows across every htmx response that follows:

csharp
public IActionResult OnPostSave(ProductEditModel m)
{
    _products.Save(m);
    this.AddToast("Product saved.", ToastLevel.Success);
    return this.PageOrPartial("_ProductsTable", _products.List());
}

#How it works

HtmxPartialResult walks the registered sync-block list on every partial render. After the primary partial it appends each sync-block partial in turn. Because each sync block carries hx-swap-oob="true" with a stable id, htmx finds the matching element in the live DOM and swaps it in place — no matter what the original hx-target was.

Context.DrainToasts() empties the per-request buffer, so a single toast shows exactly once.

  • Counter sync — the same sync-block primitive applied to unread counts, badge totals, etc.
  • Multi-target update — when you want a specific region to refresh, not every sync block.