diff --git a/README.md b/README.md
index 91c3211..c2a9a92 100644
--- a/README.md
+++ b/README.md
@@ -4,54 +4,43 @@
[](https://github.com/rameel/ramstack.htmxtoolkit/actions/workflows/test.yml)
[](LICENSE)
-HtmxToolkit integrates [HTMX](https://htmx.org/) with ASP.NET Core. It provides strongly typed APIs for request and response headers,
-MVC action filters, Razor Tag Helpers, application-wide HTMX configuration, and antiforgery support.
+HtmxToolkit helps you build [HTMX](https://htmx.org/) applications with ASP.NET Core.
-- Supports .NET 6 or later.
-- Supports HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. HTMX 2.x is selected by default.
+HTMX and the server talk through `HX-*` headers. Requests carry details such as the target element
+and the current URL, and your response can tell HTMX where to put the HTML, whether to change the URL,
+or which events to raise.
+HtmxToolkit lets you do all of this from C# and Razor, without header strings or hand-written JSON:
-See the [documentation](docs/articles/index.md) for full guides and recipes.
+- Find out whether HTMX expects a fragment or a full page, and read its headers as typed properties.
+- Tell HTMX what to do next with a fluent API: retarget, reswap, redirect, push a URL, or trigger an event.
+- Send HTMX requests to dedicated MVC actions with `[HtmxRequest]`.
+- Build `hx-get` and `hx-post` URLs from controllers, actions, and Razor Pages, just like the `asp-*` Tag Helpers do.
+- Configure HTMX once in `Program.cs` and send antiforgery tokens with HTMX requests automatically.
-## Features
+HtmxToolkit runs on .NET 6 or later and supports HTMX 1.9.x, 2.x, and 4.x. HTMX 2.x is the default.
-- Detect HTMX requests, including boosted requests, without comparing header strings.
-- Read and write all standard HTMX headers through strongly typed APIs.
-- Route HTMX requests to dedicated MVC actions with `[HtmxRequest]`.
-- Configure response behavior fluently or with `[HtmxResponse]`.
-- Generate HTMX URLs, headers, values, and request options with Razor Tag Helpers.
-- Render version-specific HTMX configuration from ASP.NET Core options.
-- Add antiforgery tokens to non-GET HTMX requests with a small companion script.
+## Getting started
-## Designed for Low Overhead
-
-`HtmxRequestHeaders` and `HtmxResponseHeaders` are `readonly` structs, each containing a single reference.
-This avoids allocating wrapper objects and allows the JIT to optimize away the wrapper overhead in inlined code.
-
-Version-specific configuration JSON is cached and reused until the configuration changes.
-Known JSON shapes use source-generated `System.Text.Json` metadata, avoiding reflection-based metadata discovery at runtime.
-
-## Installation
+**1. Install the package:**
```console
dotnet add package Ramstack.HtmxToolkit
```
-Register HtmxToolkit in `Program.cs`:
+The package doesn't include HTMX itself, so add HTMX to your application as usual,
+from a CDN or as a local file.
+
+**2. Register the toolkit in `Program.cs`:**
```csharp
using Ramstack.HtmxToolkit.Hosting;
-var builder = WebApplication.CreateBuilder(args);
-
builder.Services.AddHtmxToolkit();
```
-> [!IMPORTANT]
-> HtmxToolkit does not bundle HTMX itself. Add a supported HTMX release to the application separately.
-
-## Quick Start
+By default, the toolkit targets HTMX 2.x. If you use HTMX 1.9.x or 4.x, select it as shown in [Configuration](#configuration).
-Make the Tag Helpers and toolkit types available to Razor views in `_ViewImports.cshtml`:
+**3. Enable the Tag Helpers in `_ViewImports.cshtml`:**
```html
@using Ramstack.HtmxToolkit
@@ -59,39 +48,29 @@ Make the Tag Helpers and toolkit types available to Razor views in `_ViewImports
@addTagHelper *, Ramstack.HtmxToolkit
```
-Render the configuration metadata in the document `
`:
+**4. Update the layout.** Add `` to `` and load the Toolkit script after HTMX:
```html
+ ...
+
+ ...
+
+
+
```
-On ASP.NET Core 9 or later, enable static assets and associate them with the endpoints that render views in `Program.cs`:
-
-```csharp
-app.MapStaticAssets();
-app.MapRazorPages().WithStaticAssets();
-```
-
-> [!NOTE]
-> For MVC, apply `.WithStaticAssets()` to each controller endpoint builder that renders views
-
-On ASP.NET Core 6–8, enable static files:
-
-```csharp
-app.UseStaticFiles();
-```
-
-The NuGet package includes the toolkit script as a static web asset. Load it after HTMX in the layout:
+`` renders your HTMX settings and the antiforgery token. The Toolkit script comes
+with the package. It adds the token to HTMX requests and, with HTMX 1.x and 2.x, tells the server
+whether a request expects a fragment or a full page (see [Handling requests](#handling-requests)).
+The script is served like any other static file, so make sure the application calls `UseStaticFiles()`
+or `MapStaticAssets()`.
+See [Static assets](docs/articles/static-assets.md) for details.
-```html
-
-
-```
-
-You can now generate an HTMX URL from ASP.NET Core route information:
+**5. Send your first request.** In a view, describe where the request goes in ASP.NET Core terms:
```html
```
-- Use `hx-post`, `hx-put`, `hx-patch`, or `hx-delete` instead of `hx-get` to select another HTTP method.
-- If the method attribute is omitted, the URL Tag Helper defaults to `hx-get`.
-
-## Requests
-
-For HTMX 1.x and 2.x, use `IsHtmxRequest` to return a partial response for an HTMX request and a full-page response otherwise:
+The toolkit turns these attributes into a regular URL, and the browser receives
+`hx-get="/Books/List?category=science"`. The action returns only a piece of HTML:
```csharp
-public IActionResult Help()
+public IActionResult List(string category)
{
- if (Request.IsHtmxRequest(out var htmx) && !htmx.HistoryRestoreRequest)
- return PartialView("_Help");
-
- return View();
+ var books = _catalog.FindByCategory(category);
+ return PartialView("_BookList", books);
}
```
-Checking `HistoryRestoreRequest` ensures that history restoration receives the full page it expects.
-The `out` parameter provides strongly typed access to the request headers.
+Click the button, and the list appears in `#results` without a page reload.
+
+For a step-by-step tutorial, see [Getting started](docs/articles/getting-started.md).
+If you use Minimal APIs, see [Minimal API setup](docs/articles/minimal-apis.md).
+
+## Handling requests
-With HTMX 4, this can be written more simply:
+A page and its fragment can share one URL. HTMX gets just the fragment,
+while a direct visit or a page refresh gets the full page:
```csharp
public IActionResult Help() =>
@@ -134,51 +112,46 @@ public IActionResult Help() =>
: View();
```
-Call `Request.GetHtmxHeaders()` to access the same headers separately from request detection.
-
-Available headers and their formats vary by HTMX version; see the
-[request header comparison](docs/articles/version-compatibility.md#request-headers).
-
-`HtmxRequestHeaderNames` exposes the corresponding header-name constants for lower-level APIs.
-
-Use `Request.IsHtmxBoosted()` when only boosted navigation matters.
-Use `Request.IsHtmxFullRequest()` or `Request.IsHtmxPartialRequest()` to check the type of an HTMX request.
-Both return `false` when `HX-Request-Type` is absent, as in HTMX 1.x and 2.x.
-When other request metadata is needed, use `IsHtmxRequest(out var htmx)`.
+Checking only whether the request came from HTMX isn't enough here. A boosted link that replaces the whole ``,
+or a request with `hx-select`, comes from HTMX too, but expects a full page.
+`IsHtmxPartialRequest()` reads the `HX-Request-Type` header instead. The header is `partial` when HTMX swaps
+the response into an element inside the page, and `full` when the response replaces the whole ``
+or goes through `hx-select`. A history restore always counts as a full page.
-### MVC Action Selection
+HTMX 4 sends this header itself. HTMX 1.x and 2.x don't, so the Toolkit script adds it the same way.
+That's why the example above works with every supported version, as long as the script is on the page.
+If you cache such responses, add a `Vary` header so that the page and the fragment are cached separately.
+See [Full pages and fragments](docs/articles/pages-and-fragments.md).
-Apply `[HtmxRequest]` to reserve an action for HTMX requests:
+To read other HTMX headers, use `IsHtmxRequest`. It gives you the headers as typed properties:
```csharp
-[HttpGet("/profile/fragment")]
-[HtmxRequest]
-public IActionResult ProfileFragment()
+if (Request.IsHtmxRequest(out var htmx))
{
- return PartialView("_Profile");
+ var target = htmx.Target; // the element to update: "results", or "div#results" in HTMX 4
+ var url = htmx.CurrentUrl; // the page the user is on
}
```
-Set `BoostedFilter` to restrict action selection to boosted or non-boosted HTMX requests:
+In MVC, you can also give HTMX requests their own action. An action marked with `[HtmxRequest]`
+is selected only for HTMX requests, so two actions can share one URL:
```csharp
-[HtmxRequest(BoostedFilter = HtmxBoostedFilter.Boosted)]
-public IActionResult BoostedNavigation()
-{
- return PartialView("_Navigation");
-}
-```
+[HttpGet("/profile")]
+public IActionResult Profile() => View();
-Use `HtmxBoostedFilter.NonBoosted` for only non-boosted HTMX requests.
-The default, `HtmxBoostedFilter.Any`, does not restrict boosted navigation.
+[HttpGet("/profile")]
+[HtmxRequest(RequestType = HtmxRequestType.Partial)]
+public IActionResult ProfileFragment() => PartialView("_Profile");
+```
-Set `RequestType` to `HtmxRequestType.Full` or `HtmxRequestType.Partial` to require a matching
-`HX-Request-Type` header. Its default, `HtmxRequestType.Unspecified`, does not restrict the request type.
-When both properties are set, both conditions must match.
+HTMX requests for a fragment go to `ProfileFragment`. Everything else, including direct visits
+and boosted navigation, goes to `Profile`. The attribute can also filter by boosted navigation.
+See [Read HTMX requests](docs/articles/requests.md) for all headers and `[HtmxRequest]` options.
-## Responses
+## Controlling responses
-Configure HTMX response headers through `Response.Htmx(...)`:
+Use `Response.Htmx(...)` to tell HTMX what to do with your response:
```csharp
Response.Htmx(htmx => htmx
@@ -187,147 +160,60 @@ Response.Htmx(htmx => htmx
.TriggerEvent("profile-updated"));
```
-> [!NOTE]
-> The callback runs only for HTMX requests, so non-HTMX requests avoid unnecessary response work.
-
-The fluent API supports:
-
-- Client navigation with `Location`, `Redirect`, `PushUrl`, and `ReplaceUrl`.
-- Swap control with `Reswap`, `Retarget`, and `Reselect`.
-- Page refresh with `Refresh`.
-- Client events with `TriggerEvent`.
+The callback runs only for HTMX requests, so it is safe to call in any action.
+Besides changing the target and the swap style, you can navigate to another page (`Location`, `Redirect`),
+update the address bar (`PushUrl`, `ReplaceUrl`), reload the page (`Refresh`), and raise client-side events
+with optional data (`TriggerEvent`). This works with any `HttpResponse`, including Minimal API handlers.
-The same API works with any `HttpResponse`, including
-[Minimal API handlers](docs/articles/responses.md#configure-a-response-fluently).
-
-Use [state-passing overloads](docs/articles/responses.md#avoid-closures-in-a-hot-path) to avoid closure allocations in hot paths.
-
-For trimming and Native AOT, pass source-generated `JsonTypeInfo` metadata to `TriggerEvent`;
-see the [complete example](docs/articles/responses.md#trigger-client-events).
-
-Call `Response.GetHtmxHeaders()` for direct access to the strongly typed response headers,
-or use `HtmxResponseHeaderNames` with lower-level APIs.
-
-### Declarative Responses
-
-Controllers can set common response headers declaratively:
+In MVC controllers, common headers can also be set with an attribute:
```csharp
-[HtmxRequest]
-[HtmxResponse(
- Retarget = "#results",
- Reswap = HtmxSwap.BeforeEnd)]
-public IActionResult LoadMore()
-{
- return PartialView("_MoreResults");
-}
+[HtmxResponse(Retarget = "#results", Reswap = HtmxSwap.BeforeEnd)]
+public IActionResult LoadMore() => PartialView("_MoreResults");
```
-`HtmxResponseAttribute` supports `Refresh`, `Reswap`, `ReswapExpression`, `Retarget`, and `Reselect`.
-Use `ReswapExpression` for a complete expression with swap modifiers, such as `innerHTML show:#result:top`.
+See [Control HTMX responses](docs/articles/responses.md) for all options,
+including Native AOT support and overloads that avoid closures in hot paths.
## Tag Helpers
-HtmxToolkit includes five Tag Helpers:
-
-| Tag Helper | Purpose |
-|------------------------|-----------------------------------------------------------------------------|
-| `HtmxUrlTagHelper` | Builds HTMX request URLs from routes, controllers, actions, or Razor Pages. |
-| `HtmxHeaderTagHelper` | Serializes custom `hx-headers` values. |
-| `HtmxValsTagHelper` | Serializes additional `hx-vals` request values. |
-| `HtmxRequestTagHelper` | Generates version-specific `hx-request` or `hx-config` options. |
-| `HtmxConfigTagHelper` | Renders application configuration and antiforgery metadata. |
-
-### URL Generation
-
-Controller and action:
+The URL attributes from the example above work with Razor Pages too:
```html
-```
-
-Razor Page handler:
-
-```html
-
```
-Use `hx-all-route-data` for an `IDictionary` of route values.
-The helper also supports `hx-route`, `hx-host`, `hx-protocol`, and `hx-fragment`.
-
-### Headers and Values
+The request method comes from `hx-get`, `hx-post`, `hx-put`, `hx-patch`, or `hx-delete`.
+If you leave it out, `hx-get` is used.
-Create `hx-headers` without manually escaping JSON:
+Other Tag Helpers build HTMX attributes that otherwise need hand-written JSON:
```html
```
-Add request values in the same way:
-
-```html
-
-```
-
-Use `hx-all-headers` or `hx-all-vals` to supply an `IDictionary`.
-
-### Request Options
-
-For HTMX 1.9.x and 2.x, typed `hx-request-*` attributes generate `hx-request` JSON:
-
-```html
-
-```
-
-With HTMX 4.x selected, `HtmxRequestTagHelper` generates `hx-config` instead.
-See [Version compatibility](docs/articles/version-compatibility.md#per-request-options) for supported options.
-
-### Attribute Modifiers
-
-HTMX 1.9.x and 2.x merge-inherit `hx-request`, `hx-vals`, and `hx-headers` automatically.
-With HTMX 4.x, use the Tag Helper's `inherited` input on a parent and `append` on a child to merge their values:
-
-```html
-
-
-
-```
+| You write | You get |
+|----------------|-----------------------------------------------------------------------------------------|
+| `hx-val-*` | `hx-vals`, extra values sent with the request |
+| `hx-header-*` | `hx-headers`, extra request headers |
+| `hx-request-*` | `hx-request` (HTMX 1.x, 2.x) or `hx-config` (HTMX 4), request options such as a timeout |
-The Tag Helper emits `hx-vals:inherited` on the parent and `hx-vals:append` on the child.
-See [Attribute modifiers](docs/articles/version-compatibility.md#attribute-modifiers) for all supported inputs and version differences.
+The toolkit builds and escapes the JSON for you.
+See [Tag Helpers](docs/articles/tag-helpers.md) for the full list of attributes.
## Configuration
-Configure HTMX once during service registration. Only explicitly configured values are emitted,
-so HTMX defaults remain in effect:
+Choose the HTMX version you load in the browser and set its options in one place:
```csharp
builder.Services.AddHtmxToolkit(options =>
@@ -340,27 +226,19 @@ builder.Services.AddHtmxToolkit(options =>
});
```
-Select the version loaded by the browser with `UseHtmxV1`, `UseHtmxV2`, or `UseHtmxV4`:
-
-```csharp
-builder.Services.AddHtmxToolkit(options => options.UseHtmxV4());
-```
-
-> [!WARNING]
-> Select only one HTMX version. Attempting to select a second version in the same configuration throws an exception.
+Use `UseHtmxV1`, `UseHtmxV2`, or `UseHtmxV4`, depending on the version you use.
+Each version has its own set of options. `` renders only the options you set,
+so everything else keeps the HTMX defaults.
-See the [configuration guides](docs/articles/configuration.md) for version-specific settings and response handling.
+See [Application configuration](docs/articles/configuration.md) for the available settings.
## Antiforgery
-Antiforgery metadata is enabled by default. `` renders the current token and field or header names;
-the companion script attaches the token to non-GET HTMX requests and refreshes it after boosted navigation.
-
-> [!WARNING]
-> The companion script sends the token but does not validate it. Razor Pages validates unsafe HTTP methods automatically.
-> MVC applications must enable server-side antiforgery validation for the relevant actions.
+Antiforgery support is on by default. The Toolkit script adds the token to every HTMX request
+except GET and refreshes it after boosted navigation.
-For example, MVC applications can validate all unsafe actions globally:
+The script only sends the token; ASP.NET Core still has to check it. Razor Pages does this automatically.
+In MVC, enable validation yourself, for example globally:
```csharp
builder.Services.AddControllersWithViews(options =>
@@ -369,77 +247,65 @@ builder.Services.AddControllersWithViews(options =>
});
```
-Disable the metadata when antiforgery is handled elsewhere:
+If antiforgery is handled some other way, turn the feature off with `options.IncludeAntiforgeryToken = false`.
+See [Antiforgery](docs/articles/antiforgery.md) for more.
-```csharp
-builder.Services.AddHtmxToolkit(options =>
-{
- options.IncludeAntiforgeryToken = false;
-});
-```
+## HTMX versions
-See [Antiforgery and Toolkit script](docs/articles/antiforgery.md) for setup and script-loading details.
+Most of your code doesn't depend on the HTMX version: the toolkit adapts its output to the version you select.
+Where possible, it also smooths out the differences between versions:
-## Compatibility Notes
+- **HTMX 4 features in HTMX 1.9.x and 2.x.** The Toolkit script sends the `HX-Request-Type` header,
+ and its `ramstack-swap` extension adds the `innerMorph`, `outerMorph`, and `textContent` swap styles,
+ plus a simplified `outerSync`. To use them, enable the extension with `hx-ext` and load
+ [Idiomorph](https://github.com/bigskysoftware/idiomorph) for morphing:
-### Trigger Timing
+ ```html
+
+ ...
-> [!IMPORTANT]
-> HTMX 1.9.x and 2.x support `HX-Trigger`, `HX-Trigger-After-Swap`, and `HX-Trigger-After-Settle`.
-> HTMX 4.x supports only `HX-Trigger`, which fires when the request completes (after the swap whenever one is performed).
-> HtmxToolkit therefore emits events requested for any `HtmxTriggerTiming`
-> value through that header rather than dropping them. The `Receive` and `AfterSettle` timings cannot be preserved exactly.
+
+
+
+
+ ```
-### Morph Swaps
+ Then use the styles like any other: `hx-swap="outerMorph"` in markup or `Reswap(HtmxSwap.OuterMorph)`
+ in a response. Idiomorph is optional: without it, `innerMorph` and `outerMorph` fall back to
+ `innerHTML` and `outerHTML`, and `textContent` doesn't need it at all. HTMX 4 supports all of
+ these natively, so skip the extension there.
+- **Features that HTMX 4 dropped.** Where HTMX 4 has a close equivalent, the toolkit uses it.
+ For example, HTMX 4 has no `HX-Trigger-After-Swap` and `HX-Trigger-After-Settle` headers,
+ so events with these timings are sent in `HX-Trigger` and fire when the request completes.
-`HtmxSwap.InnerMorph` and `HtmxSwap.OuterMorph` use the native `innerMorph` and `outerMorph` swap styles in HTMX 4.x.
-No additional client-side dependency or configuration is required.
+Some differences can't be hidden. For example, in HTMX 4 an event can't wait for the settle step.
-With HTMX 1.9.x or 2.x, enable the toolkit's `ramstack-morph` extension and load Idiomorph for morphing:
+If you aren't sure which version to pick, see [Choose an HTMX version](docs/articles/choosing-version.md).
+For all the differences, see [Version compatibility](docs/articles/version-compatibility.md).
-```html
-
- ...
-
-```
-
-Without Idiomorph, the extension falls back to HTML replacement.
-See [Morph swaps](docs/articles/version-compatibility.md#morph-swaps) for script setup, supported styles, and fallback behavior.
-
-## Running Locally
-
-Building from source requires the .NET 10 SDK or later (C# 14).
-See [Development setup](CONTRIBUTING.md#development-setup) for details.
-
-### Demo
-
-The [`samples/Ramstack.HtmxToolkit.Demo`](samples/Ramstack.HtmxToolkit.Demo) project demonstrates request detection,
-response headers and events, MVC attributes, Tag Helpers, polling, boosted navigation, and antiforgery integration.
+## Demo
+The [demo application](samples/Ramstack.HtmxToolkit.Demo) shows the toolkit in action: requests and responses,
+MVC attributes, Tag Helpers, polling, boosted navigation, and antiforgery. It uses HTMX 4 and needs the .NET 10 SDK.
Run it from the repository root:
```console
dotnet run --project samples/Ramstack.HtmxToolkit.Demo
```
-The application is available at and .
+Then open .
-### Documentation
+## Documentation
-See [Building the documentation locally](docs/README.md) for build and preview commands.
+The [guides](docs/articles/index.md) cover everything from a first tutorial to recipes and troubleshooting.
## Contributing
-Bug reports and pull requests are welcome. See [Contributing](CONTRIBUTING.md) for development setup,
-validation commands, and pull request and commit guidelines.
-
-## Supported versions
-
-| | Version |
-|------|--------------------|
-| .NET | 6, 7, 8, 9, 10, 11 |
-| HTMX | 1.9.x, 2.x, 4.x |
-
+Bug reports and pull requests are welcome. See [Contributing](CONTRIBUTING.md) to learn how to build the project
+and run the tests.
## License
diff --git a/docs/README.md b/docs/README.md
index ecae667..02140ec 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -8,6 +8,8 @@ dotnet msbuild eng/Validate.proj -t:Docs -tl:off
This requires the .NET 10 SDK or later.
Documentation warnings are treated as errors.
+The target removes generated API YAML and the previous site before rebuilding, so deleted API types do not
+remain in the preview or search index. The authored `docs/api/index.md` is preserved.
Run a local preview server and open :
@@ -16,7 +18,21 @@ dotnet docfx docs/docfx.json --serve
```
DocFX writes generated HTML to `docs/_site` and generated API metadata to `docs/api`;
-both directories are ignored by Git.
+generated files in both directories are ignored by Git.
+
+## Tutorial examples
+
+Getting started, Forms and validation, Loading states and request errors, MVC, and Minimal APIs contain
+runnable examples directly in Markdown.
+When changing them, copy the listed files into a fresh application and check the documented browser flow.
+Use the current library through a project reference when validating unreleased API changes.
+
+DocFX validates documentation links and references; it does not compile fenced code or the snippets below.
+Compile changed C# and Razor examples separately. Check invalid as well as valid form submissions, a second
+submission after a swap, and the request triggered by a response event. Include padded short input such as
+`" A "` when a setter normalizes values before validation. For request feedback, check a delayed response,
+HTTP 500, a connection failure, a timeout, and a successful retry. Check the JavaScript payload listener too
+when changing event examples.
## API examples
diff --git a/docs/api-overwrites/configuration.md b/docs/api-overwrites/configuration.md
new file mode 100644
index 0000000..df612a6
--- /dev/null
+++ b/docs/api-overwrites/configuration.md
@@ -0,0 +1,34 @@
+---
+uid: Ramstack.HtmxToolkit.Hosting.ServiceCollectionExtensions.AddHtmxToolkit(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.Action{Ramstack.HtmxToolkit.Configuration.HtmxToolkitOptions})
+example:
+ - |-
+ [!code-csharp[](../snippets/configuration/RegisterToolkit.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.Configuration.HtmxToolkitOptions
+example:
+ - |-
+ [!code-csharp[](../snippets/configuration/RegisterToolkit.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.Configuration.HtmxV2Config.ResponseHandling
+example:
+ - |-
+ [!code-csharp[](../snippets/configuration/ResponseHandling.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.Configuration.ResponseHandlingConfig
+example:
+ - |-
+ [!code-csharp[](../snippets/configuration/ResponseHandling.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.Configuration.HtmxV4Config.NoSwap
+example:
+ - |-
+ [!code-csharp[](../snippets/configuration/NoSwap.cs)]
+---
diff --git a/docs/api-overwrites/requests.md b/docs/api-overwrites/requests.md
index b04e221..54df4de 100644
--- a/docs/api-overwrites/requests.md
+++ b/docs/api-overwrites/requests.md
@@ -1,5 +1,5 @@
---
-uid: Ramstack.HtmxToolkit.HttpRequestExtensions.IsHtmxRequest(Microsoft.AspNetCore.Http.HttpRequest)
+uid: Ramstack.HtmxToolkit.HttpRequestExtensions.IsHtmxPartialRequest(Microsoft.AspNetCore.Http.HttpRequest)
example:
- |-
[!code-csharp[](../snippets/requests/ReturnPartialForHtmx.cs)]
@@ -27,8 +27,22 @@ example:
---
---
-uid: Ramstack.HtmxToolkit.HtmxRequestAttribute.Kind
+uid: Ramstack.HtmxToolkit.HtmxRequestAttribute.BoostedFilter
example:
- |-
[!code-csharp[](../snippets/requests/BoostedRequestAttributeExample.cs)]
---
+
+---
+uid: Ramstack.HtmxToolkit.HtmxRequestAttribute.RequestType
+example:
+ - |-
+ [!code-csharp[](../snippets/requests/RequestTypeAttributeExample.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.HtmxRequestHeaderNames
+example:
+ - |-
+ [!code-csharp[](../snippets/requests/VaryByHtmxHeaders.cs)]
+---
diff --git a/docs/api-overwrites/responses.md b/docs/api-overwrites/responses.md
index 8e64cc9..1c92285 100644
--- a/docs/api-overwrites/responses.md
+++ b/docs/api-overwrites/responses.md
@@ -53,3 +53,70 @@ example:
- |-
[!code-csharp[](../snippets/responses/HtmxResponseAttributeExample.cs)]
---
+
+---
+uid: Ramstack.HtmxToolkit.HttpResponseExtensions.Htmx``1(Microsoft.AspNetCore.Http.HttpResponse,System.Action{Ramstack.HtmxToolkit.HtmxResponse,``0},``0)
+example:
+ - |-
+ [!code-csharp[](../snippets/responses/HtmxWithState.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.HttpResponseExtensions.GetHtmxHeaders(Microsoft.AspNetCore.Http.HttpResponse)
+example:
+ - |-
+ [!code-csharp[](../snippets/responses/ValidationTargetFilter.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.HtmxLocationOptions.Values
+example:
+ - |-
+ [!code-csharp[](../snippets/responses/LocationWithValues.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.HtmxFieldValues
+example:
+ - |-
+ [!code-csharp[](../snippets/responses/LocationWithValues.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.HtmxResponse.ReplaceUrl(System.String)
+example:
+ - |-
+ [!code-csharp[](../snippets/responses/ReplaceUrlSearch.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.HtmxResponse.PreventHistoryUpdate
+example:
+ - |-
+ [!code-csharp[](../snippets/responses/PreventHistoryUpdate.cs)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.HtmxResponse.TriggerEvent(System.String,Ramstack.HtmxToolkit.HtmxTriggerTiming)
+example:
+ - |-
+ [!code-csharp[](../snippets/responses/TriggerEventAfterSave.cs)]
+
+ [!code-razor[](../snippets/responses/TriggerEventListener.cshtml)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.HtmxSwap
+example:
+ - |-
+ [!code-csharp[](../snippets/responses/MorphSwap.cs)]
+
+ [!code-razor[](../snippets/responses/MorphSwapSetup.cshtml)]
+---
+
+---
+uid: Ramstack.HtmxToolkit.HtmxResponseHeaderNames
+example:
+ - |-
+ [!code-csharp[](../snippets/responses/ExposeResponseHeaders.cs)]
+---
diff --git a/docs/api/index.md b/docs/api/index.md
index 5e400c0..0e90dba 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -1,3 +1,20 @@
# API reference
+Find types by the task you want to perform. Each API page lists its members, accepted values, and remarks;
+the guides show how those pieces fit into an application.
+
+| Task | Main types | Guide |
+|---|---|---|
+| Detect HTMX requests and read headers | [HttpRequestExtensions](xref:Ramstack.HtmxToolkit.HttpRequestExtensions), [HtmxRequestHeaders](xref:Ramstack.HtmxToolkit.HtmxRequestHeaders) | [Requests](../articles/requests.md) |
+| Set response headers, navigation, and events | [HttpResponseExtensions](xref:Ramstack.HtmxToolkit.HttpResponseExtensions), [HtmxResponse](xref:Ramstack.HtmxToolkit.HtmxResponse), [HtmxResponseHeaders](xref:Ramstack.HtmxToolkit.HtmxResponseHeaders) | [Responses](../articles/responses.md) |
+| Set swap styles, event timing, or a follow-up request | [HtmxSwap](xref:Ramstack.HtmxToolkit.HtmxSwap), [HtmxTriggerTiming](xref:Ramstack.HtmxToolkit.HtmxTriggerTiming), [HtmxLocationOptions](xref:Ramstack.HtmxToolkit.HtmxLocationOptions) | [Response instructions](../articles/responses.md#choose-a-response-instruction) |
+| Select or configure MVC actions | [HtmxRequestAttribute](xref:Ramstack.HtmxToolkit.HtmxRequestAttribute), [HtmxResponseAttribute](xref:Ramstack.HtmxToolkit.HtmxResponseAttribute) | [MVC action selection](../articles/requests.md#select-mvc-actions-declaratively) |
+| Generate URLs in Razor | [HtmxUrlTagHelper](xref:Ramstack.HtmxToolkit.TagHelpers.HtmxUrlTagHelper) | [Request URLs](../articles/tag-helpers.md#generate-request-urls) |
+| Add values, headers, or request options | [HtmxValsTagHelper](xref:Ramstack.HtmxToolkit.TagHelpers.HtmxValsTagHelper), [HtmxHeaderTagHelper](xref:Ramstack.HtmxToolkit.TagHelpers.HtmxHeaderTagHelper), [HtmxRequestTagHelper](xref:Ramstack.HtmxToolkit.TagHelpers.HtmxRequestTagHelper) | [Tag Helpers](../articles/tag-helpers.md) |
+| Register the toolkit and render configuration | [ServiceCollectionExtensions](xref:Ramstack.HtmxToolkit.Hosting.ServiceCollectionExtensions), [HtmxToolkitOptions](xref:Ramstack.HtmxToolkit.Configuration.HtmxToolkitOptions), [HtmxConfigTagHelper](xref:Ramstack.HtmxToolkit.TagHelpers.HtmxConfigTagHelper) | [Configuration](../articles/configuration.md) |
+| Configure a specific HTMX version | [HtmxV1Config](xref:Ramstack.HtmxToolkit.Configuration.HtmxV1Config), [HtmxV2Config](xref:Ramstack.HtmxToolkit.Configuration.HtmxV2Config), [HtmxV4Config](xref:Ramstack.HtmxToolkit.Configuration.HtmxV4Config) | [Choose a version](../articles/choosing-version.md) |
+
+New to the library? Start with [Getting started](../articles/getting-started.md) for a working Razor Pages
+application, or choose the [MVC](../articles/mvc.md) or [Minimal API](../articles/minimal-apis.md) example.
+
This reference is generated from the public API and XML documentation comments in `Ramstack.HtmxToolkit`.
diff --git a/docs/articles/antiforgery.md b/docs/articles/antiforgery.md
index e77b89c..cbc05d9 100644
--- a/docs/articles/antiforgery.md
+++ b/docs/articles/antiforgery.md
@@ -1,7 +1,20 @@
# Antiforgery and Toolkit script
-The companion Toolkit script integrates ASP.NET Core antiforgery tokens with HTMX requests and supplies compatibility swaps for older HTMX versions.
-It is not the HTMX library itself.
+The Toolkit script delivers ASP.NET Core antiforgery tokens with HTMX requests. This page explains
+how that works and what the server still has to do, and lists the script's other features.
+
+## What the Toolkit script does
+
+The Toolkit script, `htmx-toolkit.min.js`, comes with the package as a static web asset.
+It is not the HTMX library itself. Load it after HTMX on every page that uses HTMX;
+see [Static assets and deployment](static-assets.md) for the URL and caching.
+
+| Feature | HTMX versions | Without the script |
+|---|---|---|
+| Adds the antiforgery token to non-GET HTMX requests and refreshes it after boosted navigation | All | Non-GET requests without a token field fail server-side antiforgery validation, usually with HTTP 400 |
+| Sends `HX-Request-Type`; see [Check full or partial requests](requests.md#check-full-or-partial-requests) | 1.x and 2.x; HTMX 4.x sends it itself | `IsHtmxPartialRequest()` and `IsHtmxFullRequest()` return `false`, and `[HtmxRequest]` with a `RequestType` does not match |
+| Replays events whose name repeats in one response header; see [Timing and repeated events](responses.md#timing-and-repeated-events) | All | None of the events in that header fire |
+| Adds the `ramstack-swap` extension with `innerMorph`, `outerMorph`, and a simplified `outerSync`, plus `textContent` for HTMX 1.9.x; see [Morph swaps](version-compatibility.md#morph-swaps) | 1.9.x and 2.x; HTMX 4.x supports these swap styles natively | These swap styles are not available |
## How automatic antiforgery works
@@ -9,73 +22,59 @@ It is not the HTMX library itself.
2. The Tag Helper writes the request token, header name, and form-field name as data attributes on ``.
3. The Toolkit script reads those attributes during page load.
4. Before each non-GET HTMX request, the script adds the token unless the form data already contains the configured antiforgery form field.
-5. ASP.NET Core validates the token normally.
+5. The application validates the token through Razor Pages, an MVC filter, or its Minimal API validation setup.
The token is sent as the configured antiforgery header when a header name is available.
Otherwise, it is added to the request parameters under the configured form-field name.
## Configure the layout
-Enable static files in `Program.cs`:
+Use the registration and layout from [Getting started](getting-started.md). Render ``
+in the document head and load HTMX before the Toolkit script. Both pieces are needed for automatic token delivery.
+See [Static assets and deployment](static-assets.md) for script URLs, caching, and `MapStaticAssets()`.
-```csharp
-var app = builder.Build();
+## Enable server-side validation
-app.UseStaticFiles();
-app.MapRazorPages();
-```
-
-On ASP.NET Core 9 or later, use `MapStaticAssets()` and associate the asset collection with the page endpoints
-instead to enable build-time compression and fingerprinted URLs:
+Sending a token and validating it are separate steps. `AddHtmxToolkit()` configures toolkit services;
+it does not add an antiforgery validation filter or middleware to your application.
-```csharp
-app.MapStaticAssets();
-app.MapRazorPages().WithStaticAssets();
-```
+### Razor Pages
-For MVC, apply `.WithStaticAssets()` to the controller endpoint builder, for example:
+Razor Pages validates unsafe requests by default. Keep that behavior enabled. The
+[forms tutorial](forms-and-validation.md) shows model binding, validation errors, and a successful save.
-```csharp
-app.MapStaticAssets();
-app.MapDefaultControllerRoute().WithStaticAssets();
-```
+### MVC
-A hybrid Razor Pages and MVC application applies it to each endpoint set that renders views:
+Enable validation for unsafe controller actions, for example with a global filter:
```csharp
-app.MapStaticAssets();
-app.MapRazorPages().WithStaticAssets();
-app.MapControllers().WithStaticAssets();
-```
+using Microsoft.AspNetCore.Mvc;
-`MapControllers()` covers attribute-routed controllers; with conventional or area routing, apply
-`.WithStaticAssets()` to each `MapControllerRoute` or `MapAreaControllerRoute` call.
-
-> [!NOTE]
-> ASP.NET Core reads the asset collection from the current endpoint's metadata. An endpoint without it still
-> renders a working URL: the resolver falls back to a `?v=...` version instead of a fingerprinted URL.
+builder.Services.AddControllersWithViews(options =>
+{
+ options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
+});
+```
-Render configuration metadata in ``, then load HTMX before the Toolkit script:
+You can instead apply `[ValidateAntiForgeryToken]` to individual POST actions, but then every relevant action
+needs that attribute. `[HtmxRequest]` only affects action selection; it does not validate a token or authorize a user.
-```html
-
-
-
-
- @RenderBody()
+### Minimal APIs
-
-
-
-```
+MVC antiforgery attributes do not protect Minimal API delegates. Register `AddAntiforgery()` and use an
+appropriate validation path for your endpoints. The [Minimal API example](minimal-apis.md) explicitly calls
+`IAntiforgery.IsRequestValidAsync()` before processing the form and returns 400 when validation fails.
-Both pieces are required for automatic antiforgery. The Tag Helper creates the token; the script attaches it to requests.
+For endpoints that bind form parameters through ASP.NET Core's built-in antiforgery support, configure
+`UseAntiforgery()` after authentication and authorization middleware when those are present. Follow the
+[ASP.NET Core antiforgery guide](https://learn.microsoft.com/aspnet/core/security/anti-request-forgery)
+for the target framework and endpoint style. Loading the Toolkit script alone does not enable server validation.
## Submit a protected form
-Razor Pages validates non-GET handlers by default:
+For a Razor Page with a `Save` POST handler:
-```html
+```razor
`.
-
-## Static web assets and caching
-
-The NuGet package includes both script variants as ASP.NET Core static web assets:
-
-```text
-/_content/Ramstack.HtmxToolkit/htmx-toolkit.min.js
-/_content/Ramstack.HtmxToolkit/htmx-toolkit.js
-```
-
-Reference the minified file from the layout with an app-relative path:
-
-```html
-
-```
+After a boosted navigation, the Toolkit script looks for antiforgery metadata in the response. If it contains
+a nonempty request token, the script updates the saved token and associated names. A fragment without token
+metadata leaves the existing values in place.
-This form requires `@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers` in `_ViewImports.cshtml`.
-ASP.NET Core resolves the `~` path and applies content-based versioning:
+Keep `` in full document layouts. After signing in, signing out, or changing identity,
+obtain a token for the new identity. A full navigation with `HX-Redirect` is a straightforward way to do that;
+see [Redirect after authentication](recipes.md#redirect-after-authentication). Keeping an old token does not refresh it.
-- On ASP.NET Core 9 or later, when the current endpoint's asset collection contains the script, the framework
- selects the fingerprinted URL, such as `htmx-toolkit.min.{fingerprint}.js`. In production, `MapStaticAssets()`
- serves fingerprinted assets with long-lived, immutable caching and supports precompressed Gzip and Brotli
- representations.
-- Otherwise, `asp-append-version="true"` appends a `?v=...` version computed and cached from the file content.
- This includes ASP.NET Core 6–8 and applications using `UseStaticFiles()`.
+## Static web assets and caching
-Both forms account for the application's path base. When the file changes, its versioned URL changes. Cache
-headers are managed by the application's static asset or static file configuration; adding `?v=...` does not
-itself set a cache lifetime.
+Script loading, versioned URLs, and deployment are covered in [Static assets and deployment](static-assets.md).
+Do not put HTML containing user-specific antiforgery tokens in a shared response cache.
+See [Caching full pages and fragments](pages-and-fragments.md#cache-full-pages-and-fragments-separately)
+when one URL can return different HTML.
-Static web assets work with both project references and NuGet packages. On publish, ASP.NET Core copies
-the scripts into the application's `wwwroot/_content/Ramstack.HtmxToolkit` directory.
+## Change the header or field name
-Request the readable script while diagnosing browser behavior:
+Configure names through ASP.NET Core's antiforgery options:
-```html
-
+```csharp
+builder.Services.AddAntiforgery(options =>
+{
+ options.HeaderName = "X-CSRF-TOKEN";
+ options.FormFieldName = "__CsrfToken";
+});
```
-If you use `defer`, apply it to both HTMX and the Toolkit script so their execution order is preserved.
+The configuration Tag Helper reads these options. You do not need to repeat the names in client-side code.
+A form field already present under the configured name takes precedence over automatic token injection.
## Disable automatic antiforgery
@@ -161,7 +148,7 @@ builder.Services.AddHtmxToolkit(options =>
});
```
-The Toolkit script can still be used for morph compatibility after antiforgery metadata is disabled.
+Keep loading the Toolkit script: its [other features](#what-the-toolkit-script-does) do not depend on antiforgery metadata.
## Security considerations
diff --git a/docs/articles/choosing-version.md b/docs/articles/choosing-version.md
index ab7e3e2..83fc222 100644
--- a/docs/articles/choosing-version.md
+++ b/docs/articles/choosing-version.md
@@ -1,7 +1,7 @@
# Choose an HTMX version
-HtmxToolkit generates version-sensitive configuration, request options, request-header access, and compatibility behavior.
-Configure the same major HTMX version that the browser loads.
+HtmxToolkit adapts configuration, request options, and response event headers to the selected HTMX version.
+Select the same major version that the browser loads.
HTMX 2.x is selected when no version is specified.
@@ -42,10 +42,10 @@ trying to select a different version causes startup validation to fail.
| Global config type | `HtmxV1Config` | `HtmxV2Config` | `HtmxV4Config` |
| Per-request attribute | `hx-request` | `hx-request` | `hx-config` |
| Triggering element header | `HX-Trigger` | `HX-Trigger` | `HX-Source` |
-| Expected response kind | Not reported | Not reported | `HX-Request-Type` |
+| Expected response kind | `HX-Request-Type` from the Toolkit script | `HX-Request-Type` from the Toolkit script | `HX-Request-Type` |
| Morph swap support | Toolkit compatibility extension | Toolkit compatibility extension | Native |
-See [Version compatibility](version-compatibility.md) for the complete application-facing differences.
+See [Version compatibility](version-compatibility.md) for the differences that affect application code.
## Configure while selecting
@@ -63,14 +63,15 @@ builder.Services.AddHtmxToolkit(options =>
});
```
-Only assigned properties are emitted. A `null` property leaves the corresponding HTMX default unchanged.
+Only settings you assign a value are included in the generated configuration. A `null` property leaves the HTMX default unchanged.
## Decide which version to use
For an existing application, select the version it already loads and review [Version compatibility](version-compatibility.md) before upgrading.
-For a new application, choose the release required by its HTMX attributes and extensions. HtmxToolkit's default is HTMX 2.x;
-choosing V4 enables the V4 request contract and its native morphing and Fetch API options.
+For a new application with no version-specific requirements, start with HTMX 2.x to follow the introductory
+tutorials without changes. It is HtmxToolkit's default target. Choose V4 when you want its native morphing
+or Fetch API options, and check that the extensions you plan to use support it.
Continue with [Application configuration](configuration.md), then use the dedicated [HTMX 1.x](configuration-v1.md),
[HTMX 2.x](configuration-v2.md), or [HTMX 4.x](configuration-v4.md) reference guide.
diff --git a/docs/articles/configuration-v1.md b/docs/articles/configuration-v1.md
index 13c2376..3d53dcd 100644
--- a/docs/articles/configuration-v1.md
+++ b/docs/articles/configuration-v1.md
@@ -32,7 +32,10 @@ Only non-null settings are written to ``.
| Requests | `WithCredentials`, `Timeout`, `SelfRequestsOnly`, `MethodsThatUseUrlParams`, `GetCacheBusterParam` |
| Navigation and focus | `ScrollBehavior`, `DefaultFocusScroll`, `ScrollIntoViewOnBoost`, `IgnoreTitle` |
| Transitions and parsing | `GlobalViewTransitions`, `UseTemplateFragments`, `TriggerSpecsCacheEnabled` |
-| WebSocket | `WsReconnectDelay`, `WsBinaryType` |
+| WebSocket | `WsBinaryType` |
+
+`wsReconnectDelay` is not exposed: in JSON, HTMX accepts only its default value `"full-jitter"`.
+To customize the WebSocket reconnection delay, assign a function to `htmx.config.wsReconnectDelay` in JavaScript.
## Common profiles
@@ -59,12 +62,12 @@ options.UseHtmxV1(config =>
## Morphing compatibility
`HtmxSwap.InnerMorph`, `OuterMorph`, and `OuterSync` are native HTMX 4.x swap styles. For HTMX 1.9.x,
-activate the `ramstack-morph` extension supplied by the Toolkit script. `InnerMorph` and `OuterMorph` can use Idiomorph
+activate the `ramstack-swap` extension supplied by the Toolkit script. `InnerMorph` and `OuterMorph` can use Idiomorph
when the application loads it; otherwise they fall back to ordinary HTML replacement. `TextContent` also uses
the compatibility extension on HTMX 1.9.x.
```html
-
+
...
```
diff --git a/docs/articles/configuration-v2.md b/docs/articles/configuration-v2.md
index 1f2eed8..097ffc0 100644
--- a/docs/articles/configuration-v2.md
+++ b/docs/articles/configuration-v2.md
@@ -31,15 +31,18 @@ builder.Services.AddHtmxToolkit(options =>
| Navigation and focus | `ScrollBehavior`, `DefaultFocusScroll`, `ScrollIntoViewOnBoost`, `IgnoreTitle` |
| Transitions and forms | `GlobalViewTransitions`, `TriggerSpecsCacheEnabled`, `ReportValidityOfForms` |
| Responses | `ResponseHandling` |
-| WebSocket | `WsReconnectDelay`, `WsBinaryType` |
+| WebSocket | `WsBinaryType` |
+
+`wsReconnectDelay` is not exposed: in JSON, the WebSocket extension accepts only its default value `"full-jitter"`.
+To customize the reconnection delay, assign a function to `htmx.config.wsReconnectDelay` in JavaScript.
## Handle validation responses
`ResponseHandling` is an ordered collection of regular-expression rules. Assigning it replaces HTMX's response-handling configuration,
so include rules for successful and error responses as well as the special response being added.
-The following example swaps a validation fragment returned with status 422, treats successful responses normally,
-and leaves other client and server errors unswapped:
+The following example swaps a validation fragment returned with status 422. The other rules repeat the HTMX 2.x defaults:
+successful responses are swapped, and other client and server errors are not:
```csharp
options.UseHtmxV2(config =>
@@ -47,7 +50,6 @@ options.UseHtmxV2(config =>
config.ResponseHandling =
[
new() { Code = "204", Swap = false },
- new() { Code = "304", Swap = false },
new() { Code = "[23]..", Swap = true },
new() { Code = "422", Swap = true, Error = false },
new() { Code = "[45]..", Swap = false, Error = true }
@@ -57,6 +59,17 @@ options.UseHtmxV2(config =>
Place a more specific rule before a broader expression when both can match.
+Use `SwapOverride` to select a typed swap style, or `SwapOverrideExpression` for a complete
+expression with modifiers or a custom swap style:
+
+```csharp
+new ResponseHandlingConfig { Code = "422", Swap = true, SwapOverride = HtmxSwap.OuterHtml };
+new ResponseHandlingConfig { Code = "422", Swap = true, SwapOverrideExpression = "outerHTML settle:200ms" };
+```
+
+Both properties use the same underlying expression and serialize as `swapOverride`.
+Assigning `SwapOverride` replaces any existing modifiers; assigning `null` clears the override.
+
## Harden fragment processing
An application that does not return executable scripts in fragments can disable script processing:
@@ -74,11 +87,11 @@ These client settings complement server-side validation, authorization, CORS, an
## Morphing compatibility
HTMX 2.x supports `TextContent` natively. To use `InnerMorph`, `OuterMorph`, or `OuterSync`,
-activate the `ramstack-morph` extension supplied by the Toolkit script. Load Idiomorph when true morphing is required;
+activate the `ramstack-swap` extension supplied by the Toolkit script. Load Idiomorph when true morphing is required;
otherwise the extension uses its documented fallback behavior.
```html
-
+
...
```
diff --git a/docs/articles/configuration-v4.md b/docs/articles/configuration-v4.md
index e4efb2c..03e6a5a 100644
--- a/docs/articles/configuration-v4.md
+++ b/docs/articles/configuration-v4.md
@@ -57,7 +57,7 @@ config.History = HtmxHistoryMode.Reload; // Perform a full page reload on hist
Choose one value; the three assignments above illustrate alternatives.
-Unlike HTMX 1.x and 2.x, HTMX 4.x does not keep local DOM snapshots in `sessionStorage` by default.
+Unlike HTMX 1.x and 2.x, HTMX 4.x does not keep local DOM snapshots in `localStorage` by default.
`Enabled` requests the history URL from the server and swaps the response.
The optional HTMX 4 `hx-history-cache` extension restores local snapshot behavior;
HtmxToolkit does not enable that extension automatically.
@@ -83,19 +83,8 @@ declares the same attribute. HtmxToolkit provides Razor-friendly boolean inputs
The parent inputs become `hx-config:inherited` and `hx-vals:inherited`. The button emits
`hx-vals:append`, which merges its values into the inherited object instead of replacing it.
-The same inputs are available for all three generated JSON attributes:
-
-| Razor Tag Helper input | Generated HTMX 4 attribute |
-|---|---|
-| `hx-request-inherited="true"` | `hx-config:inherited` |
-| `hx-request-append="true"` | `hx-config:append` |
-| `hx-vals-inherited="true"` | `hx-vals:inherited` |
-| `hx-vals-append="true"` | `hx-vals:append` |
-| `hx-headers-inherited="true"` | `hx-headers:inherited` |
-| `hx-headers-append="true"` | `hx-headers:append` |
-
-Setting both inputs for one attribute emits a combined modifier, for example
-`hx-headers:inherited:append`.
+The same inputs are available for `hx-headers`; see [Apply attribute modifiers](tag-helpers.md#apply-attribute-modifiers)
+for the full list.
Alternatively, enable the HTMX 1.x/2.x inheritance behavior globally:
diff --git a/docs/articles/configuration.md b/docs/articles/configuration.md
index dd9a487..e2f9325 100644
--- a/docs/articles/configuration.md
+++ b/docs/articles/configuration.md
@@ -24,7 +24,7 @@ builder.Services.AddHtmxToolkit(options =>
Render the selected configuration once in the document head:
-```html
+```razor
diff --git a/docs/articles/forms-and-validation.md b/docs/articles/forms-and-validation.md
new file mode 100644
index 0000000..aa6ba7f
--- /dev/null
+++ b/docs/articles/forms-and-validation.md
@@ -0,0 +1,210 @@
+# Forms, validation, and events
+
+Continue in the `HtmxStarter` project from [Getting started](getting-started.md).
+This tutorial adds a profile form that shows server validation errors, saves a display name,
+and refreshes a separate summary through a response event.
+
+The example keeps one shared profile in memory. It resets when the application restarts and is visible
+to every visitor. Replace this store with your application's user-specific persistence before using the pattern
+for real profiles.
+
+## 1. Add the input model and store
+
+Create `Models/ProfileInput.cs`:
+
+```csharp
+using System.ComponentModel.DataAnnotations;
+
+namespace HtmxStarter.Models;
+
+public sealed class ProfileInput
+{
+ private string _displayName = "";
+
+ [Required(ErrorMessage = "Enter a display name.")]
+ [StringLength(40, MinimumLength = 2, ErrorMessage = "Use 2 to 40 characters.")]
+ public string DisplayName
+ {
+ get => _displayName;
+ set => _displayName = value?.Trim() ?? "";
+ }
+}
+```
+
+The setter removes surrounding whitespace during model binding, before validation runs.
+An input such as `" A "` therefore fails the two-character minimum. Trimming only in the save handler
+would be too late: ASP.NET Core has already validated the model by then.
+
+Create `Models/ProfileStore.cs`:
+
+```csharp
+namespace HtmxStarter.Models;
+
+public sealed class ProfileStore
+{
+ public string DisplayName { get; set; } = "Ada";
+}
+```
+
+In `Program.cs`, add `using HtmxStarter.Models;` and register the store before `builder.Build()`:
+
+```csharp
+builder.Services.AddSingleton();
+```
+
+## 2. Add the form partial
+
+Create `Pages/_ProfileForm.cshtml`:
+
+```razor
+@model ProfileModel
+
+
+```
+
+`asp-for` produces the name `Input.DisplayName`, which matches the bound property in the handler below.
+`asp-validation-summary` displays errors from `ModelState`.
+
+The form replaces itself with `outerHTML`. Every returned form keeps the same ID and request attributes,
+so it can be submitted again. `novalidate` lets you see the server's validation response without the browser
+blocking an empty submission first. You can remove it when you add browser validation.
+
+The ordinary `method` and `asp-page-*` attributes provide a non-HTMX form action. The standard Form Tag Helper
+also creates a hidden antiforgery input. The Toolkit script detects that input and does not add a second token.
+For forms without that input, the layout metadata and script supply the token instead.
+
+## 3. Add the page and summary
+
+Create `Pages/_ProfileSummary.cshtml`:
+
+```razor
+@model string
+
+
+
+@await Html.PartialAsync("_ProfileForm", Model)
+
+
+```
+
+The summary is outside the form's swap target. It stays in place while the form is replaced and listens
+for a `profile-saved` event bubbling through `body`.
+
+## 4. Handle validation and saving
+
+Create `Pages/Profile.cshtml.cs`:
+
+```csharp
+using HtmxStarter.Models;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.RazorPages;
+using Ramstack.HtmxToolkit;
+
+namespace HtmxStarter.Pages;
+
+public sealed class ProfileModel : PageModel
+{
+ private readonly ProfileStore _profiles;
+
+ public ProfileModel(ProfileStore profiles) => _profiles = profiles;
+
+ [BindProperty]
+ public ProfileInput Input { get; set; } = new();
+
+ public bool Saved { get; private set; }
+
+ public string CurrentDisplayName => _profiles.DisplayName;
+
+ public void OnGet() => Input.DisplayName = _profiles.DisplayName;
+
+ public IActionResult OnGetSummary() =>
+ Partial("_ProfileSummary", _profiles.DisplayName);
+
+ public IActionResult OnPostSave()
+ {
+ if (!ModelState.IsValid)
+ return Request.IsHtmxRequest() ? Partial("_ProfileForm", this) : Page();
+
+ _profiles.DisplayName = Input.DisplayName;
+ ModelState.Clear();
+ Saved = true;
+
+ Response.Htmx(htmx => htmx.TriggerEvent(
+ "profile-saved", HtmxTriggerTiming.AfterSwap));
+
+ return Request.IsHtmxRequest() ? Partial("_ProfileForm", this) : RedirectToPage();
+ }
+}
+```
+
+Razor Pages validates the antiforgery token before the POST handler runs. Model binding then fills `Input`
+and applies its validation attributes. Invalid input returns the form with errors and leaves the store unchanged.
+
+A successful save returns a fresh form and sends `profile-saved`. HTMX receives that event, requests
+`/Profile?handler=Summary`, and updates the summary with the stored name. No custom JavaScript is needed.
+`ModelState.Clear()` lets the form show the trimmed value instead of the originally submitted text.
+
+Normal form submissions return a full page on error and redirect back after a successful save.
+
+## 5. Try both paths
+
+Run the application as in Getting started and open .
+
+1. Clear the display name and select **Save**. The form should show “Enter a display name.” and the summary should still show “Ada”.
+2. Enter a one-character name, then try `" A "` with surrounding spaces. Both should show the length error.
+3. Enter `" Grace "` and save. The error should disappear, the form should show “Profile saved.”, and both the input and summary should show “Grace” without surrounding spaces.
+4. Save a different name to check that replacing the form did not break the next submission.
+5. Disable JavaScript and reload. The form should still save using normal navigation.
+
+For a valid HTMX save, the Network panel shows a POST followed by a GET for the summary.
+With the default HTMX 2.x setup, the POST response carries `HX-Trigger-After-Swap`.
+HTMX 4.x uses `HX-Trigger` for the same Toolkit call; see [event timing](version-compatibility.md#response-events).
+
+## Why validation uses HTTP 200 here
+
+The invalid form is returned with status 200 so the default HTMX 2.x configuration displays it.
+If your application uses status 422 for validation, also configure [response handling](configuration-v2.md#handle-validation-responses).
+Changing `Retarget` or `Reswap` alone does not make HTMX 2.x swap an error response.
+
+For errors outside the form, see [Return validation errors to another target](recipes.md#return-validation-errors-to-another-target).
+For MVC and Minimal APIs, read [Antiforgery](antiforgery.md) before copying the POST pattern.
+
+## Next step
+
+Continue with [Loading states and request errors](loading-and-errors.md) to show that a save is in progress,
+disable repeated clicks, and let the user recover from a failed request.
diff --git a/docs/articles/getting-started.md b/docs/articles/getting-started.md
index 6b0dccd..173d3ca 100644
--- a/docs/articles/getting-started.md
+++ b/docs/articles/getting-started.md
@@ -1,20 +1,29 @@
# Getting started
-This tutorial adds HtmxToolkit to an ASP.NET Core Razor Pages application and loads a product fragment
-without navigating away from the page. The same registration and layout setup applies to MVC applications.
+Build a Razor Pages application that loads product details without leaving the page. You will create the
+page, its handler, and a partial view, then inspect the request in the browser.
-## 1. Install the package
+This tutorial uses the .NET 10 SDK and HTMX 2.x. The library itself supports .NET 6 or later.
+You only need basic Razor Pages knowledge; no JavaScript code is required.
+If HTMX is new to you, read [How HTMX updates a page](overview.md#how-htmx-updates-a-page) first.
+For other application styles, see [MVC](mvc.md) or [Minimal APIs](minimal-apis.md).
+
+## 1. Create the application
+
+Run these commands in the directory where you keep your projects:
```console
+dotnet new webapp -n HtmxStarter -f net10.0
+cd HtmxStarter
dotnet add package Ramstack.HtmxToolkit
```
-Add HTMX 1.9.x, 2.x, or 4.x to the application separately. HtmxToolkit does not bundle HTMX.
-This example assumes HTMX 2.x, which is the toolkit default.
+If you already have a Razor Pages application, install the package there and use your own namespace
+in the examples below.
-## 2. Register HtmxToolkit
+## 2. Register the toolkit
-Register Razor Pages and the toolkit in `Program.cs`, and enable static files:
+Replace `Program.cs` with:
```csharp
using Ramstack.HtmxToolkit.Hosting;
@@ -32,70 +41,92 @@ app.MapRazorPages();
app.Run();
```
-No configuration delegate is required for HTMX 2.x. To use another major version,
-see [Choose an HTMX version](choosing-version.md).
-
-The setup above works on ASP.NET Core 6 or later. On ASP.NET Core 9 or later, use the following instead of
-`UseStaticFiles()` and `MapRazorPages()` to enable build-time compression and fingerprinted asset URLs:
-
-```csharp
-app.MapStaticAssets();
-app.MapRazorPages().WithStaticAssets();
-```
-
-> [!NOTE]
-> In a hybrid Razor Pages and MVC application, call `.WithStaticAssets()` on every endpoint set that renders
-> views, for example `app.MapControllers().WithStaticAssets()` as well.
+`AddHtmxToolkit()` selects HTMX 2.x by default. It registers toolkit options; it does not download HTMX.
+`UseStaticFiles()` serves the Toolkit script included in the package.
+See [Static assets and deployment](static-assets.md) for `MapStaticAssets()` and production caching.
-## 3. Enable the Razor helpers
+## 3. Enable the Tag Helpers
-Add the namespace and Tag Helpers to `Pages/_ViewImports.cshtml`:
+Replace `Pages/_ViewImports.cshtml` with:
-```html
+```razor
+@using HtmxStarter
+@using HtmxStarter.Models
@using Ramstack.HtmxToolkit
+@namespace HtmxStarter.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, Ramstack.HtmxToolkit
```
-The `@using` directive makes toolkit types available.
-The first `@addTagHelper` directive enables the standard ASP.NET Core Tag Helpers used by the layout snippet
-(`~` path resolution and `asp-append-version`). The second enables attributes such as `hx-page` and `hx-route-*`,
-as well as the `` element.
+The standard ASP.NET Core helpers resolve `~/` paths and generate versioned script URLs.
+The toolkit helpers turn attributes such as `hx-page` and `hx-route-id` into HTMX request URLs.
+You will add the `HtmxStarter.Models` namespace in step 5.
-## 4. Configure the layout
+## 4. Load the browser scripts
-Render the configuration metadata in ``. Load HTMX first and the Toolkit script second:
+Replace `Pages/Shared/_Layout.cshtml` with:
-```html
+```razor
+ HtmxStarter
- @RenderBody()
-
-
-
+ @RenderBody()
+
+
+
+ @await RenderSectionAsync("Scripts", required: false)
```
-The Toolkit script is a static web asset supplied by the NuGet package. ASP.NET Core resolves the `~` path, and
-`asp-append-version="true"` makes the URL content-based: on ASP.NET Core 9 or later the framework selects a
-fingerprinted URL when available, otherwise it appends a `?v=...` version. Both forms account for the application's
-path base. Cache headers are managed by the application's static asset or static file configuration.
+The first script is HTMX, loaded from a CDN. The second is the Toolkit script, served by your application.
+Keep that order. The template's `Pages/_ViewStart.cshtml` already selects this layout.
+For everything the Toolkit script does, see [What the Toolkit script does](antiforgery.md#what-the-toolkit-script-does).
-> [!IMPORTANT]
-> `` and the Toolkit script work together to add ASP.NET Core antiforgery data to unsafe HTMX requests. Omitting either one disables that automatic behavior.
+`` renders configuration and antiforgery metadata in the document head.
+The Toolkit script uses that metadata when a form later sends a POST request.
+For local hosting of HTMX, download the same file into `wwwroot/js/htmx.min.js` and change its script URL to
+`~/js/htmx.min.js`. See the [HTMX installation guide](https://htmx.org/docs/#installing).
-## 5. Add an HTMX interaction
+## 5. Add a model and partial view
+
+Create `Models/Product.cs`:
+
+```csharp
+namespace HtmxStarter.Models;
+
+public sealed record Product(int Id, string Name, decimal Price);
+```
+
+Create `Pages/_ProductDetails.cshtml`:
+
+```razor
+@model Product
+
+
+
@Model.Name
+
Product #@Model.Id · Price: @Model.Price
+
In stock.
+
+```
+
+A partial contains only the HTML needed for an update. It has no `@page` directive and does not render
+the layout. Razor encodes model values when it writes them into HTML.
+
+## 6. Add the page and handler
Create `Pages/Products.cshtml`:
-```html
+```razor
@page
@model ProductsModel
@@ -113,35 +144,64 @@ Create `Pages/Products.cshtml`:
```
-Because an `hx-page` is present and no method was specified, `HtmxUrlTagHelper` generates an `hx-get` URL
-for the Razor Page handler.
-
-Add the handler to `Pages/Products.cshtml.cs`:
+Create `Pages/Products.cshtml.cs`:
```csharp
+using HtmxStarter.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
+namespace HtmxStarter.Pages;
+
public sealed class ProductsModel : PageModel
{
- public IActionResult OnGetDetails(int id) =>
- Content($"
Product #{id}
In stock.
", "text/html");
+ public IActionResult OnGetDetails(int id)
+ {
+ if (id != 42)
+ return NotFound();
+
+ var product = new Product(42, "Notebook", 12.50m);
+ return Partial("_ProductDetails", product);
+ }
}
```
-Run the application and select **View product**. In browser developer tools, the request contains HTMX headers
-and the returned `` is inserted into `#product-details`.
+The data is created in the handler to keep this example self-contained. In an application, load it from
+your data service before returning the partial.
-For a real application, prefer a partial view over assembling HTML in a string:
+## 7. Run it
-```csharp
-public IActionResult OnGetDetails(int id) =>
- Partial("_ProductDetails", catalog.Get(id));
+```console
+dotnet run --no-launch-profile -- --urls http://localhost:5080 --environment Development
+```
+
+Open and select **View product**. The product details should replace
+“Select a product.” without a full page load.
+
+Inspect the rendered button. Its routing attributes have become a URL similar to:
+
+```html
+
```
+Query parameter order may differ. The important parts are the page path, `id=42`, and `handler=Details`.
+
+In the browser Network panel, the request should have `HX-Request: true`, return HTTP 200,
+and contain only the `` in its response. HTMX's default swap replaces the target's contents,
+so the `#product-details` container remains available for another click.
+The request should also have `HX-Request-Type: partial`. HTMX 2.x doesn't send this header itself;
+the Toolkit script adds it, so seeing it confirms that the script is loaded.
+
+If nothing happens, start with [Clicking does not send a request](troubleshooting.md#clicking-does-not-send-a-request).
+
## Next steps
-- [Full pages and fragments](pages-and-fragments.md) makes one URL work with and without HTMX.
-- [Read HTMX requests](requests.md) explains request detection and request headers.
-- [Control HTMX responses](responses.md) lets the server change the target, swap, URL, or client behavior.
-- [Antiforgery and Toolkit script](antiforgery.md) explains protected POST requests.
+Continue with [Forms, validation, and events](forms-and-validation.md). It adds a complete POST flow
+and refreshes a second component after a successful save. Then add
+[loading states and request errors](loading-and-errors.md) to handle slow or failed requests.
+
+- [Full pages and fragments](pages-and-fragments.md) explains when to return each kind of response.
+- [Tag Helpers](tag-helpers.md) covers URL generation, headers, values, and request options.
+- [Run the examples](samples.md) points to the larger demo and its source files.
diff --git a/docs/articles/index.md b/docs/articles/index.md
index ca8fa3d..9ccd60e 100644
--- a/docs/articles/index.md
+++ b/docs/articles/index.md
@@ -1,14 +1,24 @@
ASP.NET Core + HTMX
HtmxToolkit guides
-
Build server-rendered interactions with typed requests, fluent responses, Razor Tag Helpers, version-aware configuration, and automatic antiforgery.
+
Load HTML fragments, submit forms, and update your pages with HTMX and ASP.NET Core.
-These guides focus on complete application tasks; the API reference documents individual types and members.
+Choose a path below. For individual types and members, use the [API reference](../api/index.md).
+
+New to HTMX? Read the short [Overview](overview.md) first. Then build one Razor Pages application through
+three tutorials, with each step building on the previous one:
+
+1. [Getting started](getting-started.md): load product details into the page.
+2. [Forms, validation, and events](forms-and-validation.md): save a profile and refresh its summary.
+3. [Loading states and request errors](loading-and-errors.md): show progress and recover from failed requests.
+
+For another application style, use the complete [MVC](mvc.md) or [Minimal API](minimal-apis.md) example.
+For an existing integration, browse the task guides below or [run the demo](samples.md).
## Start here
@@ -33,6 +43,8 @@ These guides focus on complete application tasks; the API reference documents in
## Build an integration
diff --git a/docs/articles/loading-and-errors.md b/docs/articles/loading-and-errors.md
new file mode 100644
index 0000000..b4e3e43
--- /dev/null
+++ b/docs/articles/loading-and-errors.md
@@ -0,0 +1,138 @@
+# Loading states and request errors
+
+Continue with the profile form from [Forms, validation, and events](forms-and-validation.md).
+This tutorial uses HTMX 2.x. It shows a loading message, disables the save button during the request,
+and displays an error when the server or connection fails.
+
+Validation errors remain part of the returned form. Request errors need a separate message because the
+default HTMX 2.x settings leave most 4xx and 5xx responses unswapped.
+
+## 1. Show when a save is running
+
+In `Pages/_ProfileForm.cshtml`, replace the opening `
```
+Razor processes the toolkit attributes on the server. The browser receives markup similar to:
+
+```html
+
+
+
+```
+
+The query parameter order may differ. `hx-page`, `hx-page-handler`, and `hx-route-*` are server-side inputs;
+HTMX in the browser uses the generated `hx-get`. `hx-target` passes through unchanged.
+
+The page handler returns a partial view:
+
```csharp
public IActionResult OnGetDetails(int id)
{
@@ -49,14 +85,33 @@ public IActionResult OnGetDetails(int id)
The Tag Helper generates the handler URL; HTMX performs the request and swap; ASP.NET Core renders the fragment.
+This is a sketch of the interaction. [Getting started](getting-started.md) supplies the model, partial,
+registration, and layout needed to run it.
+
+## Which pieces do I need?
+
+| Application feature | Setup |
+|---|---|
+| Read request headers or set ordinary response headers | Reference the package and import `Ramstack.HtmxToolkit` in C# |
+| Choose a target version or configure HTMX | Call `AddHtmxToolkit()`; render `` in Razor for browser configuration |
+| Generate URLs or JSON attributes in Razor | Register the toolkit Tag Helpers in `_ViewImports.cshtml` |
+| Attach antiforgery tokens automatically | Render configuration metadata, load the Toolkit script, and enable server validation |
+| Use compatibility morph swaps or replay duplicate event names | Load the Toolkit script; follow the relevant feature's setup |
+| Tell full-page and fragment requests apart with HTMX 1.x or 2.x | Load the Toolkit script |
+
+For HTMX 4.x, register the target version even if you only use response events, so the toolkit chooses the
+correct trigger headers. In a pure Minimal API application, HTML rendering and token metadata are supplied
+by the application; see the [Minimal API example](minimal-apis.md).
+
## Choose the integration style
-Use a shared endpoint when the full page and fragment represent the same resource. Check `Request.IsHtmxRequest()`
+Use a shared endpoint when the full page and fragment represent the same resource. Check `Request.IsHtmxPartialRequest()`
and select the appropriate result.
Use a dedicated handler when a fragment is an independent operation, such as validation, polling, or an inline edit.
-Use `[HtmxRequest]` when MVC should choose between actions based on whether the request came from HTMX.
+Use `[HtmxRequest]` when MVC should choose between actions on the same URL, for example a page action
+and a fragment action. It can match on the request type and on boosted navigation.
This is an action constraint, so it is intended for controllers rather than Razor Page handlers or Minimal APIs.
Continue with [Getting started](getting-started.md) for a complete setup.
diff --git a/docs/articles/pages-and-fragments.md b/docs/articles/pages-and-fragments.md
index e2c8fb0..b112ecb 100644
--- a/docs/articles/pages-and-fragments.md
+++ b/docs/articles/pages-and-fragments.md
@@ -4,7 +4,7 @@ An HTMX application usually returns a small HTML fragment for an HTMX request an
for normal browser navigation. Supporting both paths keeps links usable without JavaScript
and gives history restoration a full-page fallback.
-## Use one URL for both representations
+## Return a full page or a fragment from the same URL
An MVC action can select the result from the request:
@@ -13,7 +13,7 @@ public IActionResult Details(int id)
{
var product = catalog.Get(id);
- if (Request.IsHtmxRequest(out var htmx) && !htmx.HistoryRestoreRequest)
+ if (Request.IsHtmxPartialRequest())
return PartialView("_ProductDetails", product);
return View(product);
@@ -27,17 +27,20 @@ public IActionResult OnGet(int id)
{
Product = catalog.Get(id);
- if (Request.IsHtmxRequest(out var htmx) && !htmx.HistoryRestoreRequest)
+ if (Request.IsHtmxPartialRequest())
return Partial("_ProductDetails", Product);
return Page();
}
```
-When HTMX marks a request with `HX-History-Restore-Request`, return the complete document it expects to restore.
-In HTMX 1.x and 2.x this commonly follows a miss in the local snapshot cache.
-HTMX 4.x normally retrieves history entries from the server rather than keeping local DOM snapshots;
-the optional `hx-history-cache` extension restores snapshot caching.
+`IsHtmxPartialRequest()` is `true` only when HTMX swaps the response into an element inside the page.
+A boosted link that replaces the ``, an `hx-select` request, and a history restoration
+get the complete document. The method reads the `HX-Request-Type` header;
+see [Check full or partial requests](requests.md#check-full-or-partial-requests) for how it is computed.
+
+With HTMX 1.x and 2.x, the header comes from the Toolkit script, so load the script on every page that uses HTMX.
+Without it, the endpoints above return the full page to every request.
## Use a dedicated fragment endpoint
@@ -48,7 +51,7 @@ public IActionResult OnGetInventory(int productId) =>
Partial("_Inventory", inventory.Get(productId));
```
-```html
+```razor