From 5cd74ae73f199225fcce7b99abbe1c9208befbca Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 1 Sep 2026 23:30:17 +0500 Subject: [PATCH 1/6] WIP: Explore morph swap compatibility --- src/Ramstack.HtmxToolkit/HtmxSwap.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Ramstack.HtmxToolkit/HtmxSwap.cs b/src/Ramstack.HtmxToolkit/HtmxSwap.cs index ec6c78b..4019025 100644 --- a/src/Ramstack.HtmxToolkit/HtmxSwap.cs +++ b/src/Ramstack.HtmxToolkit/HtmxSwap.cs @@ -18,16 +18,30 @@ public enum HtmxSwap /// /// Morphs the inner HTML of the target element. /// + /// + /// Supported natively by HTMX 4.x. With HTMX 1.9.x or 2.x, activate the + /// ramstack-morph extension and optionally load Idiomorph. Without Idiomorph, + /// the extension falls back to innerHTML. + /// InnerMorph, /// /// Morphs the target element itself. /// + /// + /// Supported natively by HTMX 4.x. With HTMX 1.9.x or 2.x, activate the + /// ramstack-morph extension and optionally load Idiomorph. Without Idiomorph, + /// the extension falls back to outerHTML. + /// OuterMorph, /// /// Synchronizes the target element with the response. /// + /// + /// Supported natively by HTMX 4.x. With HTMX 1.9.x or 2.x, activate the + /// ramstack-morph extension to fall back to attribute synchronization and innerHTML. + /// OuterSync, /// From 05f1568d517f92711722ca0642d6d13c3af1ace8 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 3 Sep 2026 22:04:45 +0500 Subject: [PATCH 2/6] Add ramstack-morph extension to backport HTMX 4.x swap styles --- .../Assets/htmx-toolkit.js | 57 +++++++++++++++++++ .../Assets/htmx-toolkit.min.js | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js b/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js index 05f6cdf..9225690 100644 --- a/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js +++ b/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js @@ -1,4 +1,61 @@ document._r_htmx ||= ((document, htmx) => { + const warn = message => console.warn(`ramstack.htmxtoolkit: ${message}`); + + if (htmx.defineExtension) { + htmx.defineExtension("ramstack-morph", { + isInlineSwap(swap_style) { + return swap_style === "outerMorph" || swap_style === "outerSync"; + }, + handleSwap(swap_style, target, fragment) { + if (swap_style === "textContent") { + target.textContent = fragment.textContent; + return [target]; + } + + let morph_style = + swap_style === "innerMorph" ? "innerHTML" : + swap_style === "outerMorph" ? "outerHTML" : null; + + if (morph_style) { + let idiomorph = globalThis.Idiomorph; + if (idiomorph?.morph) { + return idiomorph.morph(target, fragment.children, { morphStyle: morph_style }); + } + + warn(`Idiomorph is unavailable; falling back from ${swap_style} to ${morph_style}`); + + let nodes = [...fragment.childNodes]; + morph_style === "innerHTML" + ? target.replaceChildren(...nodes) + : target.replaceWith(...nodes); + + return nodes; + } + + if (swap_style === "outerSync") { + warn("outerSync requires HTMX 4.x; falling back to attribute sync and innerHTML"); + + let source = fragment.firstElementChild; + if (source) { + for (let attr of [...target.attributes]) { + source.hasAttribute(attr.name) || target.removeAttribute(attr.name); + } + + for (let attr of source.attributes) { + target.setAttribute(attr.name, attr.value); + } + + let nodes = source.childNodes; + target.replaceChildren(...nodes); + + return nodes; + } + + } + } + }); + } + const listen = (type, listener) => { document.addEventListener(type, listener); }; diff --git a/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js b/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js index 62a98c8..a37d755 100644 --- a/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js +++ b/src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js @@ -1 +1 @@ -document._r_htmx||=((e,t)=>{const r=(t,r)=>{e.addEventListener(t,r)},a=e=>{let t=e.querySelector("meta[name='htmx-config']")?.dataset||{};return{headerName:t.antiforgeryHeaderName,formFieldName:t.antiforgeryFormFieldName,requestToken:t.antiforgeryRequestToken}};let o=a(e);const s=(e,t,r)=>{if(!/^get$/i.test(e)){const{headerName:e,formFieldName:a,requestToken:s}=o;s&&(r.has?.(a)||r[a]||(e?t[e]=s:r.set?r.set(a,s):r[a]=s))}},d=e=>{let t=(new DOMParser).parseFromString(e||"","text/html"),r=a(t);r&&(o=r)};return r("htmx:afterOnLoad",e=>{let t=e.detail;t.boosted&&d(t.xhr.responseText)}),r("htmx:after:request",e=>{let t=e.detail.ctx;t.boosted&&d(t.text)}),r("htmx:configRequest",e=>{let t=e.detail;s(t.verb,t.headers,t.parameters)}),r("htmx:config:request",e=>{let t=e.detail.ctx.request;s(t.method,t.headers,t.body)}),r("rs:events",e=>{for(let r of e.detail.value||e.detail)t.trigger(e.target,r.key,r.value)}),!0})(document,htmx); \ No newline at end of file +document._r_htmx||=((e,t)=>{const r=e=>console.warn(`ramstack.htmxtoolkit: ${e}`);t.defineExtension&&t.defineExtension("ramstack-morph",{isInlineSwap:e=>"outerMorph"===e||"outerSync"===e,handleSwap(e,t,n){if("textContent"===e)return t.textContent=n.textContent,[t];let o="innerMorph"===e?"innerHTML":"outerMorph"===e?"outerHTML":null;if(o){let a=globalThis.Idiomorph;if(a?.morph)return a.morph(t,n.children,{morphStyle:o});r(`Idiomorph is unavailable; falling back from ${e} to ${o}`);let i=[...n.childNodes];return"innerHTML"===o?t.replaceChildren(...i):t.replaceWith(...i),i}if("outerSync"===e){r("outerSync requires HTMX 4.x; falling back to attribute sync and innerHTML");let e=n.firstElementChild;if(e){for(let r of[...t.attributes])e.hasAttribute(r.name)||t.removeAttribute(r.name);for(let r of e.attributes)t.setAttribute(r.name,r.value);let r=e.childNodes;return t.replaceChildren(...r),r}}}});const n=(t,r)=>{e.addEventListener(t,r)},o=e=>{let t=e.querySelector("meta[name='htmx-config']")?.dataset||{};return{headerName:t.antiforgeryHeaderName,formFieldName:t.antiforgeryFormFieldName,requestToken:t.antiforgeryRequestToken}};let a=o(e);const i=(e,t,r)=>{if(!/^get$/i.test(e)){const{headerName:e,formFieldName:n,requestToken:o}=a;o&&(r.has?.(n)||r[n]||(e?t[e]=o:r.set?r.set(n,o):r[n]=o))}},l=e=>{let t=(new DOMParser).parseFromString(e||"","text/html"),r=o(t);r&&(a=r)};return n("htmx:afterOnLoad",e=>{let t=e.detail;t.boosted&&l(t.xhr.responseText)}),n("htmx:after:request",e=>{let t=e.detail.ctx;t.boosted&&l(t.text)}),n("htmx:configRequest",e=>{let t=e.detail;i(t.verb,t.headers,t.parameters)}),n("htmx:config:request",e=>{let t=e.detail.ctx.request;i(t.method,t.headers,t.body)}),n("rs:events",e=>{for(let r of e.detail.value||e.detail)t.trigger(e.target,r.key,r.value)}),!0})(document,htmx); \ No newline at end of file From 72c185bb42964b85b6237802d0be351ee62636f6 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 3 Sep 2026 22:07:40 +0500 Subject: [PATCH 3/6] Document HtmxSwap compatibility across HTMX versions --- src/Ramstack.HtmxToolkit/HtmxSwap.cs | 40 ++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/src/Ramstack.HtmxToolkit/HtmxSwap.cs b/src/Ramstack.HtmxToolkit/HtmxSwap.cs index 4019025..9d79d6a 100644 --- a/src/Ramstack.HtmxToolkit/HtmxSwap.cs +++ b/src/Ramstack.HtmxToolkit/HtmxSwap.cs @@ -19,9 +19,14 @@ public enum HtmxSwap /// Morphs the inner HTML of the target element. /// /// - /// Supported natively by HTMX 4.x. With HTMX 1.9.x or 2.x, activate the - /// ramstack-morph extension and optionally load Idiomorph. Without Idiomorph, - /// the extension falls back to innerHTML. + /// + /// Supported natively by HTMX 4.x. + /// + /// + /// With HTMX 1.9.x or 2.x, activate the ramstack-morph extension + /// and optionally load Idiomorph. Without Idiomorph, the extension + /// falls back to innerHTML. + /// /// InnerMorph, @@ -29,9 +34,14 @@ public enum HtmxSwap /// Morphs the target element itself. /// /// - /// Supported natively by HTMX 4.x. With HTMX 1.9.x or 2.x, activate the - /// ramstack-morph extension and optionally load Idiomorph. Without Idiomorph, - /// the extension falls back to outerHTML. + /// + /// Supported natively by HTMX 4.x. + /// + /// + /// With HTMX 1.9.x or 2.x, activate the ramstack-morph extension + /// and optionally load Idiomorph. Without Idiomorph, the extension + /// falls back to outerHTML. + /// /// OuterMorph, @@ -39,14 +49,28 @@ public enum HtmxSwap /// Synchronizes the target element with the response. /// /// - /// Supported natively by HTMX 4.x. With HTMX 1.9.x or 2.x, activate the - /// ramstack-morph extension to fall back to attribute synchronization and innerHTML. + /// + /// Supported natively by HTMX 4.x. + /// + /// + /// With HTMX 1.9.x or 2.x, activate the ramstack-morph extension + /// to fall back to attribute synchronization and innerHTML. + /// /// OuterSync, /// /// Replaces the text content of the target element. /// + /// + /// + /// Supported natively by HTMX 2.x and 4.x. + /// + /// + /// With HTMX 1.9.x, activate the ramstack-morph extension. + /// Idiomorph is not required for this style. + /// + /// TextContent, /// From 41b6f65999191688cd26786b2b85a96e942da143 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 3 Sep 2026 22:20:21 +0500 Subject: [PATCH 4/6] Document morph swap compatibility in README --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index edcb775..344ce45 100644 --- a/README.md +++ b/README.md @@ -407,6 +407,42 @@ For server-controlled polling that works with every supported HTMX version, retu 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. +### Morph Swaps + +`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. + +With HTMX 1.9.x or 2.x, enable the `ramstack-morph` extension. To preserve morphing +behavior, also load the optional `Idiomorph` core library before the first morph swap: + +```razor + + ... + + + + + +``` + +The toolkit script contains only the HTMX adapter and must be loaded after HTMX. +`Idiomorph` remains an optional dependency and is not included in the toolkit bundle. +It may be loaded before or after the toolkit script because the adapter resolves it +when each morph swap runs. Do not enable the extension with HTMX 4.x, which handles +these swap styles natively. + +If `Idiomorph` is unavailable, the adapter logs a warning and falls back from +`innerMorph` to `innerHTML` and from `outerMorph` to `outerHTML`. On HTMX 1.9.x +and 2.x, `outerSync` falls back to synchronizing the target's attributes and then +replacing its children through `innerHTML`. + +`HtmxSwap.TextContent` is also handled by the `ramstack-morph` extension and does not +require Idiomorph. It is supported natively by HTMX 2.x and 4.x; only HTMX 1.9.x +needs the extension. + ## Sample The [`samples/Ramstack.HtmxToolkit.Demo`](samples/Ramstack.HtmxToolkit.Demo) project demonstrates request detection, response headers, Tag Helpers, polling, boosted navigation, and antiforgery integration. From b71381c30694fb6729943491aabfca08989f7739 Mon Sep 17 00:00:00 2001 From: rameel Date: Thu, 3 Sep 2026 22:32:53 +0500 Subject: [PATCH 5/6] Wrap long lines in README.md --- README.md | 60 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 344ce45..582a30a 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,11 @@ [![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. +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. +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 @@ -22,9 +24,11 @@ The package targets .NET 6 and can be used by applications running on .NET 6 or 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. +- `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. +- 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 @@ -119,7 +123,8 @@ if (Request.IsHtmxRequest(out var htmx) && htmx.HistoryRestoreRequest) } ``` -Call `Request.GetHtmxHeaders()` when request detection and header access do not need to happen together. Available properties include: +Call `Request.GetHtmxHeaders()` when request detection and header access do not need to happen together. +Available properties include: - `Boosted` - `CurrentUrl` @@ -215,19 +220,20 @@ public IActionResult AddComment(CommentInput input) } ``` -`HtmxResponseAttribute` supports `Refresh`, `Reswap`, `ReswapExpression`, `Retarget`, and `Reselect`. Use `ReswapExpression` for a complete expression with swap modifiers, such as `innerHTML show:#result:top`. +`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 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. | +| 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 @@ -253,7 +259,8 @@ Razor Page handler: ``` -Use `hx-all-route-data` for an `IDictionary` of route values. The helper also supports `hx-route`, `hx-host`, `hx-protocol`, and `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`. ### Headers And Values @@ -292,11 +299,14 @@ For HTMX 1.9.x and 2.x, typed `hx-request-*` attributes generate `hx-request` JS ``` -With HTMX 4.x selected, the same Tag Helper generates `hx-config`. HTMX 4.x additionally supports `hx-request-cache`, `hx-request-redirect`, `hx-request-referrer`, `hx-request-integrity`, and `hx-request-validate`; `hx-request-no-headers` is limited to HTMX 1.9.x and 2.x. +With HTMX 4.x selected, the same Tag Helper generates `hx-config`. +HTMX 4.x additionally supports `hx-request-cache`, `hx-request-redirect`, `hx-request-referrer`, `hx-request-integrity`, +and `hx-request-validate`; `hx-request-no-headers` is limited to HTMX 1.9.x and 2.x. ## Configuration -Configure HTMX once during service registration. Only values you explicitly set are emitted, allowing HTMX defaults to remain in control: +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 => @@ -317,7 +327,9 @@ Select a supported HTMX major version with `UseHtmxV1`, `UseHtmxV2`, or `UseHtmx builder.Services.AddHtmxToolkit(options => options.UseHtmxV4()); ``` -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`. +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`. > [!WARNING] > Select only one HTMX version. Selecting another version in the same configuration throws an exception. @@ -357,7 +369,8 @@ builder.Services.AddHtmxToolkit(options => ## 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. +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 only sends the token. The application must still enable server-side antiforgery validation for the relevant endpoints. @@ -390,7 +403,9 @@ app.MapHtmxToolkitScript("/assets/htmx-toolkit.js"); ### Trigger Timing > [!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. +> 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. ### Polling @@ -405,7 +420,9 @@ For server-controlled polling that works with every supported HTMX version, retu ``` -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. +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. ### Morph Swaps @@ -445,7 +462,8 @@ needs the extension. ## Sample -The [`samples/Ramstack.HtmxToolkit.Demo`](samples/Ramstack.HtmxToolkit.Demo) project demonstrates request detection, response headers, Tag Helpers, polling, boosted navigation, and antiforgery integration. +The [`samples/Ramstack.HtmxToolkit.Demo`](samples/Ramstack.HtmxToolkit.Demo) project demonstrates request detection, +response headers, Tag Helpers, polling, boosted navigation, and antiforgery integration. Run it with: From af79b0ad722b36579744e44656b6b1d4d364a413 Mon Sep 17 00:00:00 2001 From: rameel Date: Fri, 4 Sep 2026 00:00:24 +0500 Subject: [PATCH 6/6] Refine README examples --- README.md | 179 +++++++++++++++++++++++++++++------------------------- 1 file changed, 95 insertions(+), 84 deletions(-) diff --git a/README.md b/README.md index 582a30a..256a668 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![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, +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. The package targets .NET 6 and can be used by applications running on .NET 6 or later. @@ -12,24 +12,24 @@ 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. +- 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 unsafe HTMX requests with a small companion script. +- Add antiforgery tokens to non-GET 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: +HtmxToolkit is designed to minimize HTMX integration overhead in the application's request-processing 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. +- `HtmxRequestHeaders` and `HtmxResponseHeaders` are `readonly` structs, each containing a single reference. + In normal use, they incur no wrapper allocations while preserving a strongly typed API. +- Version-specific HTMX configuration is serialized only when the configuration 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. +- Work is skipped for non-HTMX requests, and overloads that accept state allow callers to use static callbacks and avoid closure allocations. ## Installation @@ -54,14 +54,14 @@ builder.Services.AddHtmxToolkit(); Make the Tag Helpers and toolkit types available to Razor views in `_ViewImports.cshtml`: -```razor +```html @using Ramstack.HtmxToolkit @addTagHelper *, Ramstack.HtmxToolkit ``` Render the configuration metadata in the document ``: -```razor +```html @@ -75,16 +75,16 @@ app.MapHtmxToolkitScript(); Load HTMX first, then the toolkit script in the layout: -```razor +```html ``` -The default script URL contains a content hash, so it can be cached indefinitely and is invalidated automatically when the script changes. +The default script URL contains a content hash, so the script can be cached indefinitely. When the script changes, its URL changes automatically. You can now generate an HTMX URL from ASP.NET Core route information: -```razor +```html - + ``` -The toolkit script contains only the HTMX adapter and must be loaded after HTMX. -`Idiomorph` remains an optional dependency and is not included in the toolkit bundle. -It may be loaded before or after the toolkit script because the adapter resolves it -when each morph swap runs. Do not enable the extension with HTMX 4.x, which handles -these swap styles natively. +The `/profile/morph` endpoint should return the replacement root, such as `
Updated profile
`. + +The toolkit script does not bundle HTMX or Idiomorph and must be loaded after HTMX. Idiomorph remains an optional dependency +and may be loaded before or after the toolkit script because the adapter resolves it when each morph swap runs. Do not enable +the extension with HTMX 4.x, which handles these swap styles natively. -If `Idiomorph` is unavailable, the adapter logs a warning and falls back from -`innerMorph` to `innerHTML` and from `outerMorph` to `outerHTML`. On HTMX 1.9.x -and 2.x, `outerSync` falls back to synchronizing the target's attributes and then -replacing its children through `innerHTML`. +If Idiomorph is unavailable, the adapter logs a warning and falls back from `innerMorph` to `innerHTML` and from `outerMorph` +to `outerHTML`. With HTMX 1.9.x and 2.x, `outerSync` falls back to synchronizing the target's attributes and then replacing +its children using `innerHTML`. -`HtmxSwap.TextContent` is also handled by the `ramstack-morph` extension and does not -require Idiomorph. It is supported natively by HTMX 2.x and 4.x; only HTMX 1.9.x -needs the extension. +`HtmxSwap.TextContent` is also handled by the `ramstack-morph` extension and does not require Idiomorph. It is supported natively +by HTMX 2.x and 4.x; only HTMX 1.9.x needs the extension. ## Sample