Swap.Htmx 1.0.6

dotnet add package Swap.Htmx --version 1.0.6
                    
NuGet\Install-Package Swap.Htmx -Version 1.0.6
                    
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.0.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Swap.Htmx" Version="1.0.6" />
                    
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.0.6
                    
#r "nuget: Swap.Htmx, 1.0.6"
                    
#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.0.6
                    
#: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.0.6
                    
Install as a Cake Addin
#tool nuget:?package=Swap.Htmx&version=1.0.6
                    
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

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.4"></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(options =>
{
    options.ConfigureEvents(events =>
    {
        events.On(CartEvents.ItemAdded)
            .AlsoUpdate("cart-count", "_CartCount")
            .AlsoUpdate("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


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>

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

With zero configuration, the generators scan your .cshtml files:

// Auto-generated from your views
public static class SwapViews
{
    public static class Products
    {
        public const string Index = "Index";
        public const string Grid = "_Grid";
        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 (.csproj)

Add your views as additional files for the generators to scan:

<ItemGroup>
  <AdditionalFiles Include="Views\**\*.cshtml" />
  
  <AdditionalFiles Include="Modules\**\Views\**\*.cshtml" />
</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
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.

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 20 12/11/2025
1.0.5 31 12/11/2025
1.0.4 59 12/10/2025
1.0.3 53 12/9/2025
1.0.2 173 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 383 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