Swap.Htmx
0.3.2
See the version list below for details.
dotnet add package Swap.Htmx --version 0.3.2
NuGet\Install-Package Swap.Htmx -Version 0.3.2
<PackageReference Include="Swap.Htmx" Version="0.3.2" />
<PackageVersion Include="Swap.Htmx" Version="0.3.2" />
<PackageReference Include="Swap.Htmx" />
paket add Swap.Htmx --version 0.3.2
#r "nuget: Swap.Htmx, 0.3.2"
#:package Swap.Htmx@0.3.2
#addin nuget:?package=Swap.Htmx&version=0.3.2
#tool nuget:?package=Swap.Htmx&version=0.3.2
Swap.Htmx
Minimal HTMX framework for ASP.NET Core MVC that provides automatic page/partial detection, toast notifications, out-of-band swaps, and a powerful event system for decoupling domain logic from UI updates.
Features
- ✅ SwapController - Automatic page vs partial rendering based on HX-Request header
- ✅ Toast Notifications - Built-in success/error/warning/info toasts with zero JavaScript
- ✅ Out-of-Band Swaps - Update multiple page sections in one response
- ✅ Event System - Chain domain events to UI updates with static typing
- ✅ Middleware - Validates responses and headers automatically
- ✅ Extension Methods - Fluent API for HTMX headers and responses
Installation
dotnet add package Swap.Htmx
📖 Complete Setup Guide - Step-by-step instructions for setting up toasts, OOB swaps, and all features
Quick Start
1. Register Services & Middleware
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddSwapHtmx();
var app = builder.Build();
app.UseSwapHtmxShell(); // Validates HTMX responses
app.UseSwapHtmx(); // Adds event handling middleware
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
2. Create Controller
public class ProductController : SwapController
{
public async Task<IActionResult> Index()
{
var products = await _service.GetAllAsync();
return SwapView(products); // Auto-detects page vs partial
}
public async Task<IActionResult> Create(ProductDto dto)
{
await _service.CreateAsync(dto);
// Show success toast
Response.ShowSuccessToast("Product created!");
return SwapView("Success");
}
}
3. Create View
@model List<Product>
<div id="product-list">
<h1>Products</h1>
@foreach (var product in Model)
{
<div class="product-card">
<h3>@product.Name</h3>
<p>$@product.Price</p>
<button hx-post="/products/delete/@product.Id"
hx-target="#product-list"
hx-confirm="Delete this product?">
Delete
</button>
</div>
}
</div>
Core Concepts
SwapView() - Automatic Rendering
SwapView() automatically returns the correct response type:
public async Task<IActionResult> Details(int id)
{
var product = await _service.GetAsync(id);
// Initial page load: Returns View() with layout
// HTMX request: Returns PartialView() without layout
return SwapView("Details", product);
}
How it works:
- Checks for
HX-Requestheader - HTMX request →
PartialView()(no layout) - Normal request →
View()(with layout) - Adds
Vary: HX-Requestheader for caching
Toast Notifications
Show user feedback with simple extension methods:
Response.ShowSuccessToast("Product saved!");
Response.ShowErrorToast("Something went wrong!");
Response.ShowWarningToast("Please review your changes.");
Response.ShowInfoToast("Processing in background...");
Features:
- 4 toast types with different colors
- Auto-dismiss after 3 seconds
- Configurable positioning (top-right, bottom-right, etc.)
- Multiple toasts stack vertically
- Pure HTMX - no JavaScript required
Out-of-Band (OOB) Swaps
Update multiple page sections in a single response:
public async Task<IActionResult> AddToCart(int productId)
{
await _cartService.AddItemAsync(productId);
// Main content
var main = SwapView("ItemAdded");
// Also update cart total in header (out-of-band)
var total = await _cartService.GetTotalAsync();
ViewData["OobCartTotal"] = $@"
<div id=""cart-total"" hx-swap-oob=""true"">
{total.ItemCount} items - ${total.Total}
</div>";
return main;
}
Common use cases:
- Update header badge counts
- Refresh sidebar panels
- Update multiple dashboard widgets
- Sync item in list after editing details
Event System
Chain domain events to UI updates without coupling:
// Define event keys (static typing enforced)
public static class ProductEvents
{
public static readonly EventKey Created = new("product.created");
public static readonly EventKey Updated = new("product.updated");
}
public static class UiEvents
{
public static readonly EventKey RefreshList = new("ui.refreshList");
public static readonly EventKey ShowToast = new("ui.toast.success");
}
// Configure event chains
builder.Services.AddSwapHtmx(events =>
{
// When product is created, refresh list and show toast
events.Chain(ProductEvents.Created,
UiEvents.RefreshList,
UiEvents.ShowToast);
});
// In controller, emit domain event
public async Task<IActionResult> Create(ProductDto dto)
{
await _service.CreateAsync(dto);
// Emit domain event (triggers UI events via chain)
await _publisher.EmitAsync(ProductEvents.Created);
return SwapView("Success");
}
📖 Full Event System Documentation
Extension Methods
Request Extensions
if (Request.IsHtmxRequest())
{
// Handle HTMX request
}
if (Request.IsBoosted())
{
// Handle boosted request
}
var currentUrl = Request.GetCurrentUrl();
var target = Request.GetHtmxTarget();
Response Extensions
// Set HX-Redirect
Response.HxRedirect("/products");
// Set HX-Refresh
Response.HxRefresh();
// Set HX-Location with context
Response.HxLocation("/products/details/1", new { target = "#main" });
// Trigger client-side events
Response.HxTrigger("productUpdated");
Response.HxTrigger(new { showModal = new { id = 123 } });
// Set HX-Retarget
Response.HxRetarget("#different-element");
// Set HX-Reswap
Response.HxReswap("outerHTML");
Testing
The framework includes comprehensive test coverage:
- 38 Unit Tests - Verify methods, headers, event chains
- 16 E2E Tests - Playwright tests in real browsers
- 6 toast tests
- 5 OOB swap tests
- 4 combined feature tests
- 1 debug test
# Run unit tests
cd framework/Swap.Htmx.Tests
dotnet test
# Run E2E tests (requires test app running)
cd framework/Swap.Htmx.E2ETests
dotnet test
Documentation
Getting Started
- Complete Setup Guide - Step-by-step setup for new projects
Features
- Toast Notifications - Complete toast API and examples
- Out-of-Band Swaps - Multiple element updates
- Event System - Domain event → UI event chains
Reference
- Templates - Project templates and patterns
- E2E Testing - Browser-based testing guide
Examples
See the test app for complete working examples:
cd framework/Swap.Htmx.TestApp/src
dotnet run
# Visit http://localhost:5000/test
Philosophy
Swap.Htmx is minimal by design:
- Automatic View Rendering -
SwapView()handles page vs partial logic - Domain→UI Event Mapping - Emit domain events, UI updates follow
- HTMX-Native - Leverage HTMX's capabilities, don't fight them
- Static Typing - No magic strings for event names
Everything else is just sensible defaults and extension methods.
Requirements
- .NET 8.0 or higher
- ASP.NET Core MVC
- HTMX 2.0+ (via CDN or npm)
License
MIT License - see LICENSE file for details.
Contributing
See CONTRIBUTING.md for guidelines.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- RabbitMQ.Client (>= 6.8.1)
- Swap.Modularity (>= 0.1.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.6 | 50 | 12/11/2025 |
| 1.0.5 | 56 | 12/11/2025 |
| 1.0.4 | 79 | 12/10/2025 |
| 1.0.3 | 73 | 12/9/2025 |
| 1.0.2 | 193 | 12/5/2025 |
| 1.0.1 | 680 | 12/2/2025 |
| 1.0.0 | 521 | 11/27/2025 |
| 0.14.0 | 164 | 11/26/2025 |
| 0.13.0 | 163 | 11/26/2025 |
| 0.12.0 | 166 | 11/26/2025 |
| 0.11.4 | 170 | 11/25/2025 |
| 0.11.3 | 175 | 11/24/2025 |
| 0.11.2 | 344 | 11/21/2025 |
| 0.11.1 | 275 | 11/21/2025 |
| 0.11.0 | 300 | 11/21/2025 |
| 0.10.0 | 341 | 11/21/2025 |
| 0.9.1 | 385 | 11/20/2025 |
| 0.9.0 | 379 | 11/20/2025 |
| 0.8.2 | 386 | 11/20/2025 |
| 0.8.1 | 388 | 11/20/2025 |
| 0.8.0 | 379 | 11/20/2025 |
| 0.7.1 | 381 | 11/20/2025 |
| 0.7.0 | 385 | 11/20/2025 |
| 0.6.0 | 386 | 11/20/2025 |
| 0.5.1 | 384 | 11/19/2025 |
| 0.5.0 | 317 | 11/17/2025 |
| 0.4.1 | 321 | 11/17/2025 |
| 0.4.0 | 266 | 11/16/2025 |
| 0.3.5 | 242 | 11/14/2025 |
| 0.3.4 | 281 | 11/12/2025 |
| 0.3.3 | 264 | 11/12/2025 |
| 0.3.2 | 275 | 11/11/2025 |
| 0.3.1 | 189 | 11/6/2025 |
| 0.3.0 | 190 | 11/3/2025 |
| 0.2.0-dev | 120 | 11/1/2025 |
| 0.1.0 | 177 | 10/30/2025 |