Swap.Htmx 1.3.0

dotnet add package Swap.Htmx --version 1.3.0
                    
NuGet\Install-Package Swap.Htmx -Version 1.3.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Swap.Htmx" Version="1.3.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Swap.Htmx" Version="1.3.0" />
                    
Directory.Packages.props
<PackageReference Include="Swap.Htmx" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Swap.Htmx --version 1.3.0
                    
#r "nuget: Swap.Htmx, 1.3.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Swap.Htmx@1.3.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Swap.Htmx&version=1.3.0
                    
Install as a Cake Addin
#tool nuget:?package=Swap.Htmx&version=1.3.0
                    
Install as a Cake Tool

Swap.Htmx

NuGet License: MIT

HTMX + ASP.NET Core, made simple.

Build interactive web apps with server-rendered HTML. No JavaScript frameworks, no complex state management, no build tools.


Quick Start (5 minutes)

1. Install

dotnet add package Swap.Htmx

Optional (realtime + Redis):

dotnet add package Swap.Htmx.Realtime
dotnet add package Swap.Htmx.Realtime.Redis

2. Setup (Program.cs)

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddSwapHtmx();  // ← Add this

var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseSwapHtmx();              // ← Add this
app.MapControllers();
app.Run();

3. Layout (_Layout.cshtml)

<head>
    <link rel="stylesheet" href="~/_content/Swap.Htmx/css/swap.css" />
    <script src="https://unpkg.com/htmx.org@2.0.8"></script>
    <script src="~/_content/Swap.Htmx/js/swap.client.js"></script>
</head>
<body>
    @RenderBody()
</body>

4. Your First Controller

public class ProductsController : SwapController
{
    public IActionResult Index()
    {
        var products = GetProducts();
        return SwapView(products);  // Auto-detects HTMX vs full page
    }

    [HttpPost]
    public IActionResult Add(Product product)
    {
        SaveProduct(product);
        
        return SwapResponse()
            .WithView("_ProductRow", product)           // Main response
            .AlsoUpdate("product-count", "_Count", GetCount())  // OOB update
            .WithSuccessToast("Product added!")         // Toast notification
            .Build();
    }
}

That's it! You're ready to build interactive UIs.


Core Concepts

1. SwapController & SwapView

SwapController auto-detects HTMX requests:

public class HomeController : SwapController
{
    public IActionResult Index()
    {
        // Normal request → View with layout
        // HTMX request  → PartialView (no layout)
        return SwapView(model);
    }
}

Don't want to inherit? Use extensions:

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return this.SwapView(model);  // Extension method
    }
}

2. SwapResponse Builder

For complex responses with multiple updates:

return SwapResponse()
    .WithView("_MainContent", model)              // Primary response
    .AlsoUpdate("sidebar", "_Sidebar", sidebar)   // OOB swap
    .AlsoUpdate("header", "_Header", header)      // Another OOB
    .WithSuccessToast("Done!")                    // Toast
    .WithTrigger("dataUpdated")                   // Client event
    .Build();

3. Swap Navigation

SPA-style navigation without JavaScript:


<a href="/products" hx-get="/products" hx-target="#main" hx-push-url="true">Products</a>


<swap-nav to="/products">Products</swap-nav>

Configure the default target:

builder.Services.AddSwapHtmx(options =>
{
    options.DefaultNavigationTarget = "#main-content";
});

4. SwapState (Server-Driven State)

Manage UI state with hidden fields—no JavaScript state management:

Define state:

public class FilterState : SwapState
{
    public string Category { get; set; } = "all";
    public int Page { get; set; } = 1;
    public string? Search { get; set; }
}

Render in view:

<swap-state state="Model.State" />

<input type="text" 
       name="Search"
       value="@Model.State.Search"
       hx-get="/Products/Filter?Page=1"
       hx-target="#results"
       hx-include="#filter-state"
       hx-trigger="keyup changed delay:300ms" />

Bind in controller:

[HttpGet]
public IActionResult Filter([FromSwapState] FilterState state)
{
    var products = GetProducts(state.Category, state.Search, state.Page);
    return PartialView("_ProductList", new ViewModel { State = state, Products = products });
}

Key Pattern: URL parameters override hidden fields (first value wins).

📖 Full SwapState Guide


5. Event System

Three approaches, from simple to powerful:

A. Direct Builder (Simple)
// You know exactly what to update
return SwapResponse()
    .WithView("_Item", item)
    .AlsoUpdate("count", "_Count", count)
    .Build();
B. Event Configuration (Medium)
// Configure once in Program.cs
builder.Services.AddSwapHtmx(events =>
{
    events.When(CartEvents.ItemAdded)
        .RefreshPartial("cart-count", "_CartCount")
        .RefreshPartial("cart-total", "_CartTotal");
});

// Controller just fires event
return SwapEvent(CartEvents.ItemAdded, item).WithView("_Added", item).Build();
C. Event Handlers (Powerful) ⭐
// Define events
public static class TaskEvents
{
    public static readonly EventKey Completed = new("task.completed");
}

// Handler updates stats (DI supported!)
[SwapHandler(typeof(TaskEvents), nameof(TaskEvents.Completed))]
public class StatsHandler : ISwapEventHandler<TaskPayload>
{
    private readonly IStatsService _stats;
    public StatsHandler(IStatsService stats) => _stats = stats;
    
    public void Handle(SwapEventContext<TaskPayload> context)
    {
        var stats = _stats.Calculate();
        context.Response.AlsoUpdate("stats-panel", "_Stats", stats);
    }
}

// Handler updates activity feed
[SwapHandler(typeof(TaskEvents), nameof(TaskEvents.Completed))]
public class ActivityHandler : ISwapEventHandler<TaskPayload>
{
    public void Handle(SwapEventContext<TaskPayload> context)
    {
        context.Response.AlsoUpdate("activity", "_Activity", GetRecent());
    }
}

// Controller stays thin
public IActionResult Complete(int id)
{
    var task = _service.Complete(id);
    return SwapEvent(TaskEvents.Completed, new TaskPayload(task))
        .WithView("_TaskCompleted", task)
        .Build();
}
// One event → multiple handlers → one response with all updates

📖 Full Events Guide

If you’re using SSE/WebSockets, see: 📖 Event Naming & Realtime Routing


6. When to Use OOB Swaps

✅ Use OOB for related updates:

// Add to cart → update count AND total
return SwapResponse()
    .WithView("_ProductAdded", product)
    .AlsoUpdate("cart-count", "_Count", count)
    .AlsoUpdate("cart-total", "_Total", total)
    .Build();

❌ Don't stuff unrelated updates:

// BAD: Kitchen sink response
return SwapResponse()
    .WithView("_Item", item)
    .AlsoUpdate("header", "_Header", header)
    .AlsoUpdate("sidebar", "_Sidebar", sidebar)
    .AlsoUpdate("footer", "_Footer", footer)
    .AlsoUpdate("notifications", "_Notifications", notifications)
    // ... 10 more unrelated things
    .Build();

Instead: Use event handlers or let components refresh themselves:

<div hx-get="/notifications" hx-trigger="load, every 30s"></div>

7. Performance & Security

OOB swaps render in parallel. When a response contains multiple AlsoUpdate() calls, all partial views are rendered concurrently via Task.WhenAll(). A dashboard with 12+ OOB swaps benefits from this — ordering is preserved.

Target IDs are validated. IDs must start with a letter and contain only letters, digits, hyphens, and underscores. Invalid IDs (XSS payloads, empty strings, special characters) throw ArgumentException at build time, not at render time.

Redirect URLs are validated. WithRedirect() and WithNavigation() reject javascript:, data:, and vbscript: URL schemes.


Feature Reference

Feature Usage
Auto HTMX detection SwapView() / this.SwapView()
Multiple updates SwapResponse().AlsoUpdate()
SPA navigation <swap-nav to="/path">
State management <swap-state> + [FromSwapState]
Toast notifications .WithSuccessToast(), .WithErrorToast()
Client events .WithTrigger("eventName")
Event handlers ISwapEventHandler<T>
Form validation <swap-validation> + SwapValidationErrors()
Real-time (SSE) ServerSentEvents()
Real-time (WebSocket) WebSocket registry
Source generators [SwapEventSource], auto SwapViews/SwapElements

Source Generators

Eliminate magic strings with compile-time code generation:

1. Type-Safe Event Keys

// Define your events
[SwapEventSource]
public static partial class CartEvents
{
    public const string ItemAdded = "cart.itemAdded";
    public const string CheckoutCompleted = "cart.checkoutCompleted";
}

// Generated at build time:
// CartEvents.Cart.ItemAdded          → EventKey("cart.itemAdded")
// CartEvents.Cart.CheckoutCompleted  → EventKey("cart.checkoutCompleted")

// Use in controller
return SwapEvent(CartEvents.Cart.ItemAdded, item).Build();

2. Auto-Generated View & Element Constants (Zero Config)

With zero configuration, the generators scan your .cshtml files and group by controller folder:

// Auto-generated from your views
public static class SwapViews
{
    public static class Products
    {
        public const string Index = "Index";
        public const string Details = "Details";
        public const string _Grid = "_Grid";           // Partials keep underscore
        public const string _Pagination = "_Pagination";
    }
}

public static class SwapElements
{
    public const string ProductGrid = "product-grid";
    public const string CartCount = "cart-count";
}

// Use instead of magic strings
builder.AlsoUpdate(SwapElements.CartCount, SwapViews.Cart._Count, count);

3. Setup (Zero Config!)

No configuration required! The Swap.Htmx.targets auto-includes common folders:

  • Views/**/*.cshtml
  • Modules/**/Views/**/*.cshtml
  • Pages/**/*.cshtml
  • Components/**/*.cshtml
  • Areas/**/Views/**/*.cshtml

Just reference Swap.Htmx and build — views are scanned automatically.

Optional: To inspect generated code:

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>obj\Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
  <Compile Remove="obj\Generated\**\*.cs" />
</ItemGroup>

4. Compile-Time Validation

The HandlerValidationAnalyzer warns you about:

  • SWAP001: Events without handlers
  • SWAP002: Undefined event keys
  • SWAP003: Circular event chains

📖 Full Source Generators Guide


Documentation

Guide Description
Getting Started Full setup walkthrough
Public API & Compatibility What is stable vs experimental
Security Checklist CSRF, realtime auth, room scoping, headers
SwapState Server-driven state management
Events Event system deep dive
Navigation SPA-style navigation
Patterns Common patterns cheatsheet
Real-time SSE & WebSocket
Validation Form validation

Demos

Demo Description
SwapStateDemo State management patterns
SwapLab Pattern showcase
SwapShop E-commerce example
SwapDashboard Dashboard with events
SwapSmallPartials Complex UI orchestration

Philosophy

  1. HTML is the source of truth — State lives in the DOM, not JavaScript
  2. Server renders everything — No client-side templating
  3. One response, many updates — OOB swaps coordinate UI
  4. Events decouple UI from logic — Controllers don't know about layout
  5. No build tools — Just .NET and HTML

License

MIT License - see LICENSE

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  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 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Swap.Htmx:

Package Downloads
Swap.Htmx.Realtime

Realtime primitives for Swap.Htmx: Server-Sent Events (SSE), WebSockets, connection registry, event bridge, and middleware. (Redis backplane lives in Swap.Htmx.Realtime.Redis.)

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.3.0 87 3/4/2026
1.2.0 243 1/19/2026
1.1.1 212 1/13/2026
1.1.0 342 12/19/2025
1.0.6 439 12/11/2025
1.0.5 428 12/11/2025
1.0.4 451 12/10/2025
1.0.3 445 12/9/2025
1.0.2 276 12/5/2025
1.0.1 715 12/2/2025
1.0.0 556 11/27/2025
0.14.0 199 11/26/2025
0.13.0 193 11/26/2025
0.12.0 202 11/26/2025
0.11.4 196 11/25/2025
0.11.3 210 11/24/2025
0.11.2 376 11/21/2025
0.11.1 301 11/21/2025
0.11.0 329 11/21/2025
0.10.0 366 11/21/2025
Loading failed