diff --git a/README.md b/README.md index d75e6ad..edcb775 100644 --- a/README.md +++ b/README.md @@ -1,662 +1,273 @@ # HtmxToolkit -[![NuGet](https://img.shields.io/nuget/v/Ramstack.HtmxToolkit.svg)](https://nuget.org/packages/Ramstack.HtmxToolkit) -[![MIT](https://img.shields.io/github/license/rameel/ramstack.htmxtoolkit)](https://github.com/rameel/ramstack.htmxtoolkit/blob/main/LICENSE) - -Provides HTMX integration for ASP.NET Core applications. - - -* [HtmxToolkit](#htmxtoolkit) - * [Getting Started](#getting-started) - * [HttpRequest](#httprequest) - * [HtmxRequestAttribute](#htmxrequestattribute) - * [HttpResponse](#httpresponse) - * [The declarative way of setting response headers](#the-declarative-way-of-setting-response-headers) - * [Polling](#polling) - * [Tag Helpers](#tag-helpers) - * [HtmxUrlTagHelper](#htmxurltaghelper) - * [HtmxHeaderTagHelper](#htmxheadertaghelper) - * [HtmxValsTagHelper](#htmxvalstaghelper) - * [HtmxRequestTagHelper](#htmxrequesttaghelper) - * [HtmxConfigTagHelper](#htmxconfigtaghelper) - * [Response Handling Configuration](#response-handling-configuration) - * [Toolkit Script](#toolkit-script) - * [Supported Versions](#supported-versions) - * [Contributions](#contributions) - * [License](#license) - - -## Getting Started - -Add the [`Ramstack.HtmxToolkit` NuGet package](https://www.nuget.org/packages/Ramstack.HtmxToolkit/) -to your project with the following command: + +[![NuGet](https://img.shields.io/nuget/v/Ramstack.HtmxToolkit.svg)](https://www.nuget.org/packages/Ramstack.HtmxToolkit/) +[![Build](https://github.com/rameel/ramstack.htmxtoolkit/actions/workflows/test.yml/badge.svg)](https://github.com/rameel/ramstack.htmxtoolkit/actions/workflows/test.yml) +[![License: MIT](https://img.shields.io/github/license/rameel/ramstack.htmxtoolkit)](LICENSE) + +HtmxToolkit connects [HTMX](https://htmx.org/) with ASP.NET Core. It adds strongly typed request and response headers, MVC action filters, Razor Tag Helpers, application-wide HTMX configuration, and antiforgery support. + +The package targets .NET 6 and can be used by applications running on .NET 6 or later. It supports HTMX 1.9.x, HTMX 2.x, and HTMX 4.x. HTMX 2.x is selected by default. + +## Features + +- Detect HTMX and 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 unsafe HTMX requests with a small companion script. + +## Designed for Low Overhead + +HtmxToolkit is designed to make HTMX integration inexpensive on the application's request path: + +- `HtmxRequestHeaders` and `HtmxResponseHeaders` are readonly, single-reference structs. In normal use they add no wrapper allocation while preserving a strongly typed API. +- Version-specific HTMX configuration is serialized only when it changes; the resulting JSON is cached and reused across requests. +- Known JSON shapes use source-generated `System.Text.Json` metadata, avoiding reflection-based metadata discovery at runtime. Event details passed to `TriggerEvent` are the deliberate exception because their types are defined by the application. +- Work is skipped for non-HTMX requests, and state-passing overloads allow static callbacks when callers need to avoid closure allocations. + +## Installation ```console dotnet add package Ramstack.HtmxToolkit ``` -Register the toolkit. HTMX 2.x is used by default: +Register HtmxToolkit in `Program.cs`: ```csharp +using Ramstack.HtmxToolkit.Hosting; + +var builder = WebApplication.CreateBuilder(args); + builder.Services.AddHtmxToolkit(); ``` -To select another major version or override HTMX defaults, see -[`HtmxConfigTagHelper`](#htmxconfigtaghelper). +> [!IMPORTANT] +> HtmxToolkit does not bundle HTMX itself. Add a supported HTMX release to the application separately. -## HttpRequest +## Quick Start -The library provides the `HttpRequestExtensions` class for working with `HttpRequest`. +Make the Tag Helpers and toolkit types available to Razor views in `_ViewImports.cshtml`: -```csharp -/// -/// Provides extension methods for the class. -/// -public static class HttpRequestExtensions -{ - /// - /// Determines whether the specified HTTP request is an HTMX request. - /// - /// The HTTP request. - /// - /// if the specified HTTP request is an HTMX request; - /// otherwise, . - /// - public static bool IsHtmxRequest(this HttpRequest request); - - /// - /// Determines whether the specified HTTP request is an HTMX request. - /// - /// The HTTP request. - /// When this method returns, contains the - /// that provides access to well-known HTMX headers. - /// - /// if the specified HTTP request is an HTMX request; otherwise, . - /// - public static bool IsHtmxRequest(this HttpRequest request, out HtmxRequestHeaders headers); - - /// - /// Determines whether the specified HTTP request was made using AJAX - /// instead of a normal navigation. - /// - /// The HTTP request. - /// - /// if the specified HTTP request is boosted; otherwise, . - /// - public static bool IsHtmxBoosted(this HttpRequest request); - - /// - /// Determines whether the specified HTTP request was made using AJAX - /// instead of a normal navigation. - /// - /// The HTTP request. - /// When this method returns, contains the - /// that provides access to well-known HTMX headers. - /// - /// if the specified HTTP request is boosted; otherwise, . - /// - public static bool IsHtmxBoosted(this HttpRequest request, out HtmxRequestHeaders headers); - - /// - /// Returns a strongly typed view of the HTMX request headers. - /// - /// The HTTP request. - /// - /// The . - /// - public static HtmxRequestHeaders GetHtmxHeaders(this HttpRequest request); -} +```razor +@using Ramstack.HtmxToolkit +@addTagHelper *, Ramstack.HtmxToolkit ``` -Use `IsHtmxRequest` to determine whether the current request was issued by HTMX. +Render the configuration metadata in the document ``: -```csharp -HttpContext.Request.IsHtmxRequest() +```razor + + + ``` -You can then handle HTMX and regular requests differently, for example: +Map the companion script endpoint in `Program.cs`: ```csharp -if (Request.IsHtmxRequest()) - return PartialView(); - -return View(); +app.MapHtmxToolkitScript(); ``` -The overloads with an `out` parameter also provide access to strongly typed headers set by HTMX: +Load HTMX first, then the toolkit script in the layout: -```csharp -if (Request.IsHtmxRequest(out var headers)) -{ - if (headers.HistoryRestoreRequest) - { - ... - } -} +```razor + + ``` -You can also access strongly typed headers by calling `GetHtmxHeaders`: +The default script URL contains a content hash, so it can be cached indefinitely and is invalidated automatically when the script changes. -```csharp -var headers = Request.GetHtmxHeaders(); -``` +You can now generate an HTMX URL from ASP.NET Core route information: -The complete set of request header properties is shown below: +```razor + -```csharp -/// -/// Represents strongly typed HTMX request headers. -/// -public readonly struct HtmxRequestHeaders -{ - /// - /// Gets a value indicating whether the request was made using AJAX instead of a normal navigation. - /// - public bool Boosted { get; } - - /// - /// Gets the current URL of the browser. - /// - public string? CurrentUrl { get; } - - /// - /// Gets a value indicating whether the request restores history after a miss in the local history cache. - /// - public bool HistoryRestoreRequest { get; } - - /// - /// Gets the user's response to an hx-prompt on the client. - /// - public string? Prompt { get; } - - /// - /// Gets a value indicating whether the current request is an HTMX request. - /// - public bool Request { get; } - - /// - /// Gets the ID of the target element, if present. - /// - public string? Target { get; } - - /// - /// Gets the name of the triggered element, if present. - /// - public string? TriggerName { get; } - - /// - /// Gets the ID of the triggered element, if present. - /// - public string? Trigger { get; } -} +
``` -For example: +If no HTTP method is specified, the URL Tag Helper emits `hx-get`. Use `hx-post`, `hx-put`, `hx-patch`, or `hx-delete` to select another method. -```csharp -if (Request.GetHtmxHeaders().HistoryRestoreRequest) -{ - ... -} -``` +## Requests -The `HtmxRequestHeaderNames` class also provides constants for well-known request header names, -so you do not have to remember their exact spelling. +Use `IsHtmxRequest()` when an endpoint should return a partial response to HTMX and a complete page to a normal navigation: ```csharp -/// -/// Defines constants for the well-known names of HTMX request headers. -/// -/// -/// For more information, see HTMX Request Headers Reference. -/// -public static class HtmxRequestHeaderNames +using Ramstack.HtmxToolkit; + +public IActionResult Details(int id) { - /// - /// The HX-Boosted header indicates whether the request was made using AJAX - /// instead of a normal navigation. - /// - public const string Boosted = "HX-Boosted"; - - /// - /// The HX-Current-URL header contains the current URL of the browser. - /// - public const string CurrentUrl = "HX-Current-URL"; - - ... - // The list of other constants is omitted for brevity + var model = repository.Find(id); + + return Request.IsHtmxRequest() + ? PartialView("_Details", model) + : View(model); } ``` -### HtmxRequestAttribute - -To route HTMX requests to a specific controller action, apply the `HtmxRequestAttribute` -action constraint to that action: +The overload with an `out` parameter returns a strongly typed view of the request headers: ```csharp -public class UserController : ControllerBase +if (Request.IsHtmxRequest(out var htmx) && htmx.HistoryRestoreRequest) { - [HtmxRequest] - public IActionResult UpdateProfile(UserProfile profile) - { - ... - } + // Handle a history cache miss. } ``` -To match only boosted requests, set the `Boosted` property to `true`: +Call `Request.GetHtmxHeaders()` when request detection and header access do not need to happen together. Available properties include: -```csharp -public class UserController : ControllerBase -{ - ... - [HtmxRequest(Boosted = true)] - public IActionResult UpdateProfile(UserProfile profile) - { - ... - } -} -``` +- `Boosted` +- `CurrentUrl` +- `HistoryRestoreRequest` +- `Prompt` +- `Request` +- `Target` +- `Trigger` +- `TriggerName` -## HttpResponse +`HtmxRequestHeaderNames` exposes the corresponding header-name constants for lower-level APIs. -For working with response headers, the library provides the `HttpResponseExtensions` class: +Use `Request.IsHtmxBoosted()` when only boosted navigation matters. It also has an overload that returns the typed headers. -```csharp -/// -/// Provides extension methods for the class. -/// -public static class HttpResponseExtensions -{ - /// - /// Returns a strongly typed view of the HTMX response headers. - /// - /// The HTTP response. - /// - /// The . - /// - public static HtmxResponseHeaders GetHtmxHeaders(this HttpResponse response); - - /// - /// Configures the HTMX response headers. - /// - /// The HTTP response to configure. - /// The delegate that configures the HTMX response headers. - public static void Htmx(this HttpResponse response, Action configure); - - /// - /// Configures the HTMX response headers. - /// - /// The HTTP response to configure. - /// The delegate that configures the HTMX response headers - /// using . - /// The state passed to . - public static void Htmx(this HttpResponse response, Action configure, TState state); -} -``` +### MVC Action Selection -The `GetHtmxHeaders` method provides access to strongly typed response headers -that control HTMX behavior. +Apply `[HtmxRequest]` to route only HTMX requests to an action: ```csharp -/// -/// Represents strongly typed HTMX response headers. -/// -public readonly struct HtmxResponseHeaders +[HtmxRequest] +public IActionResult UpdateProfile(ProfileInput input) { - /// - /// Gets or sets the value of the HX-Location header, which performs - /// a client-side redirect without a full-page reload. - /// - [MaybeNull] - public string Location { get; set; } - - /// - /// Gets or sets the value of the HX-Push-Url header, which pushes a new URL - /// onto the browser's history stack. - /// - [MaybeNull] - public string PushUrl { get; set; } - - ... - // The remaining properties are omitted for brevity + var profile = repository.Update(input); + return PartialView("_Profile", profile); } ``` -Just as `HtmxRequestHeaderNames` defines constants for HTMX request headers, -`HtmxResponseHeaderNames` defines constants for HTMX response headers. +Set `Boosted` to distinguish boosted and non-boosted HTMX requests: ```csharp -/// -/// Defines constants for the well-known names of HTMX response headers. -/// -/// -/// For more information, see HTMX Response Headers Reference. -/// -public static class HtmxResponseHeaderNames +[HtmxRequest(Boosted = true)] +public IActionResult BoostedNavigation() { - /// - /// The HX-Location header performs a client-side redirect without a full-page reload. - /// - public const string Location = "HX-Location"; - - /// - /// The HX-Push-Url header pushes a new URL onto the browser's history stack. - /// - public const string PushUrl = "HX-Push-Url"; - - /// - /// The HX-Redirect header performs a client-side redirect to a new location. - /// - public const string Redirect = "HX-Redirect"; - - ... - // The list of other constants is omitted for brevity + return PartialView("_Navigation"); } ``` -The most convenient approach is to use one of the `HttpResponse.Htmx` extension methods. -Its callback receives an `HtmxResponse`, allowing you to configure response headers in a fluent style: +## Responses + +Configure HTMX response headers through `Response.Htmx(...)`: ```csharp -Response.Htmx(h => h - .TriggerEvent( - eventName: "process", - detail: new { Value = ... })); +Response.Htmx(htmx => htmx + .Retarget("#profile") + .Reswap(HtmxSwap.OuterHtml) + .TriggerEvent("profile-updated", new { id = profile.Id })); ``` -`TriggerEvent` and `TriggerEvents` accept an optional `HtmxTriggerTiming` value. -In HTMX 1.x and 2.x, the value selects `HX-Trigger`, `HX-Trigger-After-Swap`, or -`HX-Trigger-After-Settle`. HTMX 4.x supports only `HX-Trigger`, so the toolkit -emits events requested for any timing through that header rather than dropping -them. These events run after the swap; in particular, the 1.x/2.x `Receive` and -`AfterSettle` timings cannot be preserved. See -[htmx pull request #3900](https://github.com/bigskysoftware/htmx/pull/3900) for -the upstream timing change. +> [!NOTE] +> The callback runs only for an HTMX request, so regular requests avoid unnecessary response work. -:bulb: The generic overload accepts an additional state parameter to avoid closure allocations: +The fluent API supports: -```csharp -Response.Htmx( - static (h, value) => h - .TriggerEvent( - eventName: "process", - detail: new { Value = value }), - ProcessValue); -``` +- Client navigation with `Location`, `Redirect`, `PushUrl`, and `ReplaceUrl`. +- Swap control with `Reswap`, `Retarget`, and `Reselect`. +- Page refresh with `Refresh`. +- Client events with `TriggerEvent` and `TriggerEvents`. -:bulb: The same API works in Minimal API handlers by binding `HttpResponse`: +The same API works in Minimal API handlers: ```csharp app.MapGet("/profile", (HttpResponse response) => { - response.Htmx(h => h.Retarget("#profile")); + response.Htmx(htmx => htmx.Retarget("#profile")); return TypedResults.Content("
Profile
", "text/html"); }); ``` -In all these examples, headers are set only for an HTMX request. For a regular request, -the callback passed to `Htmx` is not executed, avoiding unnecessary work. - -### The declarative way of setting response headers - -Some response headers can be set declaratively by applying `HtmxResponseAttribute` -to a controller or action. For example, an action that renders one new comment can append -it to the element targeted by the request: +> [!TIP] +> For a callback that captures state, use the generic overload to avoid a closure allocation. ```csharp -public class CommentController : Controller -{ - [HtmxRequest] - [HtmxResponse(Reswap = HtmxSwap.BeforeEnd)] - public IActionResult Add(CommentInput input) - { - var comment = ...; - return PartialView("_Comment", comment); - } -} -``` - -:bulb: For a more complex swap expression, such as `innerHTML show:#result:top`, -use the `Reswap` overload that accepts a string. - -```csharp -/// -/// Sets the HX-Reswap header to specify how the response will be swapped. -/// -/// The swap style to assign to the header. -/// -/// The current instance. -/// -public HtmxResponse Reswap(HtmxSwap value); - -/// -/// Sets the HX-Reswap header to specify how the response will be swapped. -/// -/// The header value to set. -/// -/// The current instance. -/// -public HtmxResponse Reswap(string value); -``` - -For declarative configuration, `HtmxResponseAttribute` provides the `ReswapExpression` property: - -```csharp -/// -/// Gets or sets the complete HX-Reswap header value, including any swap modifiers. -/// -[MaybeNull] -public string ReswapExpression { get; set; } - -/// -/// Gets or sets the swap style to specify in the HX-Reswap header. -/// -public HtmxSwap Reswap { get; set; } -``` - -Use `ReswapExpression` when the strongly typed `Reswap` property is not flexible enough. - -## Polling - -For server-controlled polling that works in every supported HTMX version, return -the polling element itself and replace it with `outerHTML`: - -```html -
- Polling... -
+Response.Htmx( + static (htmx, id) => htmx.TriggerEvent("profile-updated", new { id }), + profile.Id); ``` -While polling should continue, return the same element with its request -attributes. To stop, return the element without `hx-get` and `hx-trigger`: - -```html -
- Polling stopped! -
-``` +Call `Response.GetHtmxHeaders()` for direct strongly typed access, or use `HtmxResponseHeaderNames` with lower-level APIs. -This load-polling pattern gives the server control over every next request and -works with HTMX 1.9.x, 2.x, and 4.x. For an indefinitely updated status, use -`hx-trigger="every 1s"` instead. +### Declarative Responses -HTMX 1.9.x and 2.x also recognize HTTP status code `286` as a fixed-rate polling -stop signal. Applications that target only those versions can opt into that legacy -behavior directly: +Controllers can set common response headers declaratively: ```csharp -Response.StatusCode = 286; +[HtmxRequest] +[HtmxResponse( + Retarget = "#comments", + Reswap = HtmxSwap.BeforeEnd)] +public IActionResult AddComment(CommentInput input) +{ + var comment = repository.Add(input); + return PartialView("_Comment", comment); +} ``` -HTMX 4.x treats `286` as a regular successful response, so it is not exposed as a -toolkit API. +`HtmxResponseAttribute` supports `Refresh`, `Reswap`, `ReswapExpression`, `Retarget`, and `Reselect`. Use `ReswapExpression` for a complete expression with swap modifiers, such as `innerHTML show:#result:top`. ## Tag Helpers -The library provides five tag helpers: - -* `HtmxUrlTagHelper` -* `HtmxHeaderTagHelper` -* `HtmxValsTagHelper` -* `HtmxRequestTagHelper` -* `HtmxConfigTagHelper` - -To make them available in your project, add the `@addTagHelper` directive to a Razor view: - -```razor -@addTagHelper *, Ramstack.HtmxToolkit -``` - -To make the tag helpers available throughout the application, add this line to -`_ViewImports.cshtml`, which is inherited by Razor views by default. - -Import the toolkit namespace there as well if a view refers to toolkit types: - -```razor -@using Ramstack.HtmxToolkit -``` - -### HtmxUrlTagHelper - -The `HtmxUrlTagHelper` generates URLs for HTMX requests in much the same way that -the built-in ASP.NET Core tag helpers generate links. In most cases, replace the `asp-` prefix -with `hx-`: - -```razor -
- -
-``` - -The following code will be generated: - -```html -
- -
-``` - -If no HTMX method is specified, the tag helper uses `hx-get`. You can select a method with -`hx-get`, `hx-post`, `hx-put`, `hx-delete`, or `hx-patch`. - -For instance, in the following example, we use `hx-post`: - -```razor -
- -
-``` +HtmxToolkit includes five Tag Helpers: -In this case, the following code will be generated: +| 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. | -```html -
- -
-``` +### URL Generation -Use `hx-page` and `hx-page-handler` to generate a URL for a Razor Page handler: +Controller and action: ```razor -
- -
-``` - -The following code will be generated: - -```html -
- -
+ ``` -The `hx-all-route-data` attribute accepts an `IDictionary` containing -additional route values: +Razor Page handler: ```razor -@{ - var parameters = new Dictionary - { - ["category"] = "science", - ["pdf"] = "true" - }; -} - - -``` - -The following code will be generated: - -```html - + ``` -The following URL-generation attributes are also available: - -* `hx-host` -* `hx-protocol` -* `hx-fragment` +Use `hx-all-route-data` for an `IDictionary` of route values. The helper also supports `hx-route`, `hx-host`, `hx-protocol`, and `hx-fragment`. -### HtmxHeaderTagHelper +### Headers And Values -HTMX lets you add custom request headers through a JSON-valued attribute. Because writing and -escaping that JSON manually can be inconvenient, `HtmxHeaderTagHelper` provides a clearer format: +Create `hx-headers` without manually escaping JSON: ```razor -
- Get some HTML and include custom headers in the request -
-``` - -The following code will be generated: - -```html -
- Get some HTML and include custom headers in the request -
-``` - -You can also assign an `IDictionary` to `hx-all-headers`: - -```razor -@{ - var headers = new Dictionary - { - ["Key-1"] = "Value-1", - ["Key-2"] = "Value-2" - }; -} - -
- Get some HTML and include custom headers in the request -
+ ``` -`HtmxHeaderTagHelper` handles JSON serialization and escaping. - -### HtmxValsTagHelper - -The `HtmxValsTagHelper` adds values that HTMX includes with a request. Use `hx-val-*` -attributes instead of writing JSON manually: +Add request values in the same way: ```razor -``` - -You can also assign an `IDictionary` to `hx-all-vals`. - -### HtmxRequestTagHelper +### Request Options -`HtmxRequestTagHelper` configures request options for the selected HTMX version. -For HTMX 1.9.x and 2.x, use typed `hx-request-*` attributes instead of writing `hx-request` -JSON manually: +For HTMX 1.9.x and 2.x, typed `hx-request-*` attributes generate `hx-request` JSON: ```razor -``` +## Configuration -For HTMX 4.x, the tag helper generates `hx-config`. In addition to `timeout` and `credentials`, -HTMX 4.x supports `cache`, `redirect`, `referrer`, `integrity`, and `validate`. The `noHeaders` -option is available only in HTMX 1.9.x and 2.x. - -```razor - -``` - -With HTMX 4.x selected, the following HTML will be generated: - -```html - -``` - -### HtmxConfigTagHelper - -HTMX configuration is defined at application startup through `AddHtmxToolkit`. -The values apply application-wide and override HTMX defaults, so configure only behavior -the application relies on. For example, a form-oriented application can report native -validation failures before sending a request and scroll restored focus into view after a swap: +Configure HTMX once during service registration. Only values you explicitly set are emitted, allowing HTMX defaults to remain in control: ```csharp builder.Services.AddHtmxToolkit(options => @@ -742,58 +309,22 @@ builder.Services.AddHtmxToolkit(options => }); ``` -Use the tag helper as a marker where the configuration meta element should be rendered: +Render `` in the document `` to produce the corresponding `` element. -```html - - - -``` - -The following markup will be generated: - -```html - - - -``` - -The marker can also be written as a `meta` element: - -```html - -``` - -HTMX 2.x is selected by default. Use `UseHtmxV1`, `UseHtmxV2`, or `UseHtmxV4` to select a -version explicitly. Each configuration type follows the names used by that HTMX version, so HTMX 1.9.x -and 2.x expose `DefaultSwapStyle` and `Timeout`, while HTMX 4.x exposes `DefaultSwap` and -`DefaultTimeout`. Selecting different versions in the same configuration throws an exception. - -To target HTMX 4.x, select it explicitly: +Select a supported HTMX major version with `UseHtmxV1`, `UseHtmxV2`, or `UseHtmxV4`: ```csharp builder.Services.AddHtmxToolkit(options => options.UseHtmxV4()); ``` -The configured values remain available through dependency injection: +Configuration property names follow the selected HTMX release. For example, HTMX 1.9.x and 2.x use `DefaultSwapStyle` and `Timeout`, while HTMX 4.x uses `DefaultSwap` and `DefaultTimeout`. -```csharp -public sealed class ConfigurationInspector(IOptions options) -{ - public HtmxV2Config HtmxConfig => - options.Value.GetHtmxConfig(); -} -``` +> [!WARNING] +> Select only one HTMX version. Selecting another version in the same configuration throws an exception. -#### Response Handling Configuration +### Response Handling -HTMX 2.x introduces the [`responseHandling`](https://htmx.org/docs/#response-handling) configuration option, -allowing you to define how HTMX should handle responses based on HTTP status codes. Rules are -configured in order through `HtmxV2Config`: +HTMX 2.x can customize response handling by status code: ```csharp builder.Services.AddHtmxToolkit(options => @@ -812,8 +343,7 @@ builder.Services.AddHtmxToolkit(options => }); ``` -HTMX 4.x removes `responseHandling`. To retain HTMX 2.x behavior that does not swap error -responses, configure `NoSwap` explicitly: +HTMX 4.x replaces `responseHandling` with `noSwap`. Configure equivalent rules explicitly when migrating: ```csharp builder.Services.AddHtmxToolkit(options => @@ -825,17 +355,14 @@ builder.Services.AddHtmxToolkit(options => }); ``` -## Toolkit Script +## Antiforgery -The toolkit script provides antiforgery support and HTMX compatibility behavior. -Antiforgery metadata generation is enabled by default. Include the script to ensure the -token is added to non-GET request form parameters or headers and refreshed in a timely manner. +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. -Sending the token does not enable server-side validation by itself. Configure antiforgery -validation for the corresponding ASP.NET Core endpoints as appropriate. +> [!WARNING] +> The companion script only sends the token. The application must still enable server-side antiforgery validation for the relevant endpoints. -To disable antiforgery metadata generation—for example, when the application handles -antiforgery separately or does not issue unsafe HTMX requests—set the option to `false`: +Disable the metadata when antiforgery is handled elsewhere: ```csharp builder.Services.AddHtmxToolkit(options => @@ -844,73 +371,61 @@ builder.Services.AddHtmxToolkit(options => }); ``` -You can embed the minified script directly in a Razor view: +Instead of mapping an endpoint, the companion script can be embedded directly: ```razor ``` -Pass `true` to embed the debug version instead: - -```razor - -``` - -The minified version is used by default and is less than 1 KB. - -The method returns a cached `HtmlString`, avoiding repeated conversions and allocations. - -Alternatively, register an endpoint that serves the script: +Pass `debug: true` to `HtmxToolkitScript` or `HtmxToolkitScriptPath` to use the readable script during development. A custom endpoint path is also supported: ```csharp -app.UseAuthorization(); -... -app.MapHtmxToolkitScript(); -app.MapControllers(); +app.MapHtmxToolkitScript("/assets/htmx-toolkit.js"); ``` -By default, the registered path is mapped to `/htmxtoolkit/[sha1-hash]`, -where **[sha1-hash]** represents a precomputed hash of the script content. -The hash changes whenever the script changes, providing automatic cache invalidation. +## Compatibility Notes -To use a custom path, pass it to `MapHtmxToolkitScript`: +### Trigger Timing -```csharp -app.MapHtmxToolkitScript("/my-path"); -``` +> [!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`, so HtmxToolkit emits events requested for any `HtmxTriggerTiming` through that header rather than dropping them. The exact receive/settle timing cannot be preserved on HTMX 4.x. -Then include the mapped script in a Razor view: +### Polling -```razor - +For server-controlled polling that works with every supported HTMX version, return the polling element itself and replace it with `outerHTML`: + +```html +
+ Polling... +
``` -Pass `true` to generate a path with the `?debug` query string and load the debug version: +Return the same element with its request attributes to continue polling, or return it without `hx-get` and `hx-trigger` to stop. Status code `286` stops polling in HTMX 1.9.x and 2.x, but HTMX 4.x treats it as a regular successful response. -```razor - -``` +## Sample -Without the `debug` argument, the endpoint serves the minified version. +The [`samples/Ramstack.HtmxToolkit.Demo`](samples/Ramstack.HtmxToolkit.Demo) project demonstrates request detection, response headers, Tag Helpers, polling, boosted navigation, and antiforgery integration. -## Supported Versions +Run it with: -The following .NET and HTMX versions are supported: +```console +dotnet run --project samples/Ramstack.HtmxToolkit.Demo +``` -| | Version | -|------|----------------------------------| -| .NET | 6, 7, 8, 9, 10, 11 | -| HTMX | 1.9.x, 2.x (default), 4.x (beta) | +## Contributing -## Contributions +Bug reports and pull requests are welcome. To validate a change locally: -Bug reports and contributions are welcome. +```console +dotnet build +dotnet test +``` ## License -This package is released as open source under the **MIT License**. -See the [LICENSE](https://github.com/rameel/ramstack.htmxtoolkit/blob/main/LICENSE) file for more details. +HtmxToolkit is available under the [MIT License](LICENSE).