From c0ce9039e439c6f8d2637bfaffe6334eb0d5c7b7 Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 3 Nov 2026 02:38:09 +0500 Subject: [PATCH 1/2] config: require selecting the HTMX version HtmxToolkit selected HTMX 2.x when the application chose no version. It cannot detect the script that the browser loads, so an application on HTMX 1.9.x or 4.x silently got markup and response headers for the wrong version. Moving the default to HTMX 4.x later would break existing applications just as silently. Make the AddHtmxToolkit delegate required and fail at startup if no version is selected. Without AddHtmxToolkit, nothing can be validated at startup, so HtmxConfig and TargetVersion throw on first use. This also removes the HTMX 2.x fallback for response events and the side effect of selecting a version by reading HtmxConfig. --- .../Configuration/HtmxToolkitOptions.cs | 42 ++++++++++++----- .../Hosting/ServiceCollectionExtensions.cs | 15 +++++-- .../Internal/PendingEvents.cs | 10 ++--- .../Configuration/HtmxToolkitOptionsTests.cs | 45 +++++-------------- .../HtmxResponseTests.cs | 16 +++++++ .../TagHelpers/HtmxConfigTagHelperTests.cs | 12 ++--- 6 files changed, 81 insertions(+), 59 deletions(-) diff --git a/src/Ramstack.HtmxToolkit/Configuration/HtmxToolkitOptions.cs b/src/Ramstack.HtmxToolkit/Configuration/HtmxToolkitOptions.cs index ef4909b..dda7942 100644 --- a/src/Ramstack.HtmxToolkit/Configuration/HtmxToolkitOptions.cs +++ b/src/Ramstack.HtmxToolkit/Configuration/HtmxToolkitOptions.cs @@ -8,28 +8,37 @@ namespace Ramstack.HtmxToolkit.Configuration; /// public sealed class HtmxToolkitOptions { + /// + /// The message reported when no HTMX version has been selected. + /// + internal const string VersionNotSelectedMessage = + "The HTMX version is not selected. Call UseHtmxV1, UseHtmxV2, or UseHtmxV4 in AddHtmxToolkit."; + private HtmxConfig? _config; /// /// Gets the configuration for the selected HTMX major version. /// /// - /// If no version has been selected, reading this property selects HTMX 2.x. - /// A later call to or then throws - /// . Reading - /// or calling has the same effect. + /// Throws if no version has been selected with + /// , , or . /// public HtmxConfig HtmxConfig { get { if (_config is null) - UseHtmxV2(); + Error_VersionNotSelected(); return _config; } } + /// + /// Gets a value indicating whether an HTMX version has been selected. + /// + internal bool IsVersionSelected => _config is not null; + /// /// Gets or sets a value indicating whether antiforgery request metadata is rendered /// by the configuration tag helper. @@ -41,8 +50,14 @@ public HtmxConfig HtmxConfig /// Gets the HTMX major version used for version-sensitive generated markup and response headers. /// /// - /// For example, with HTMX 4.x, - /// emits events with any value through the HX-Trigger header. + /// + /// For example, with HTMX 4.x, + /// emits events with any value through the HX-Trigger header. + /// + /// + /// Throws if no version has been selected, + /// as when reading . + /// /// public HtmxTargetVersion TargetVersion => HtmxConfig.TargetVersion; @@ -99,9 +114,8 @@ public HtmxToolkitOptions UseHtmxV4(Action? configure = null) /// The requested configuration instance. /// /// - /// If no version has been selected, HTMX 2.x is selected, as when reading . - /// Throws if the configuration of the selected version - /// is not a . + /// Throws if no version has been selected, + /// or if the configuration of the selected version is not a . /// public TConfig GetHtmxConfig() where TConfig : HtmxConfig { @@ -148,6 +162,14 @@ private void EnsureCanSelectVersion(HtmxTargetVersion version) Error_ReconfigureTargetVersion(current, version); } + /// + /// Throws an when no HTMX version has been selected. + /// + /// Always thrown. + [DoesNotReturn] + private static void Error_VersionNotSelected() => + throw new InvalidOperationException(VersionNotSelectedMessage); + /// /// Throws an when the requested HTMX /// configuration type does not match the configured target version. diff --git a/src/Ramstack.HtmxToolkit/Hosting/ServiceCollectionExtensions.cs b/src/Ramstack.HtmxToolkit/Hosting/ServiceCollectionExtensions.cs index cd10b97..85fa6cc 100644 --- a/src/Ramstack.HtmxToolkit/Hosting/ServiceCollectionExtensions.cs +++ b/src/Ramstack.HtmxToolkit/Hosting/ServiceCollectionExtensions.cs @@ -13,18 +13,25 @@ public static class ServiceCollectionExtensions /// Registers and configures HtmxToolkit services. /// /// The service collection. - /// An optional delegate used to configure HtmxToolkit. + /// The delegate used to configure HtmxToolkit. /// /// The same collection. /// - public static IServiceCollection AddHtmxToolkit(this IServiceCollection services, Action? configure = null) + /// + /// The delegate must select the HTMX version with , + /// , or ; + /// otherwise, the application fails to start. + /// + public static IServiceCollection AddHtmxToolkit(this IServiceCollection services, Action configure) { services .AddOptions() + .Validate( + static options => options.IsVersionSelected, + HtmxToolkitOptions.VersionNotSelectedMessage) .ValidateOnStart(); - if (configure is not null) - services.Configure(configure); + services.Configure(configure); return services; } diff --git a/src/Ramstack.HtmxToolkit/Internal/PendingEvents.cs b/src/Ramstack.HtmxToolkit/Internal/PendingEvents.cs index 7c1ba50..e869409 100644 --- a/src/Ramstack.HtmxToolkit/Internal/PendingEvents.cs +++ b/src/Ramstack.HtmxToolkit/Internal/PendingEvents.cs @@ -4,6 +4,7 @@ using System.Text.Json; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Ramstack.HtmxToolkit.Configuration; @@ -289,8 +290,7 @@ private HtmxTriggerTiming NormalizeTiming(HtmxTriggerTiming timing) => : timing; /// - /// Returns the configured HTMX target version, or HTMX 2.x if - /// of is not registered. + /// Returns the configured HTMX target version. /// /// The response whose request services are inspected. /// @@ -298,10 +298,8 @@ private HtmxTriggerTiming NormalizeTiming(HtmxTriggerTiming timing) => /// private static HtmxTargetVersion GetTargetVersion(HttpResponse response) { - var p = response.HttpContext.RequestServices; - var options = p.GetService(typeof(IOptions)) as IOptions; - - return options?.Value.TargetVersion ?? HtmxTargetVersion.V2; + var options = response.HttpContext.RequestServices.GetRequiredService>(); + return options.Value.TargetVersion; } #region Inner type: EventList diff --git a/tests/Ramstack.HtmxToolkit.Tests/Configuration/HtmxToolkitOptionsTests.cs b/tests/Ramstack.HtmxToolkit.Tests/Configuration/HtmxToolkitOptionsTests.cs index adc4c3a..2b274cf 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/Configuration/HtmxToolkitOptionsTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/Configuration/HtmxToolkitOptionsTests.cs @@ -10,15 +10,14 @@ namespace Ramstack.HtmxToolkit.Tests.Configuration; public class HtmxToolkitOptionsTests { [Test] - public void DefaultsToHtmx2() + public void HtmxConfig_WithoutSelectedVersion_Throws() { var options = new HtmxToolkitOptions(); - Assert.Multiple(() => - { - Assert.That(options.TargetVersion, Is.EqualTo(HtmxTargetVersion.V2)); - Assert.That(options.HtmxConfig, Is.TypeOf()); - }); + Assert.That( + () => options.HtmxConfig, + Throws.TypeOf() + .With.Message.EqualTo("The HTMX version is not selected. Call UseHtmxV1, UseHtmxV2, or UseHtmxV4 in AddHtmxToolkit.")); } [Test] @@ -96,24 +95,6 @@ public void GetHtmxConfig_ForDifferentVersion_Throws() ); } - [Test] - public void UseHtmxV4_AfterReadingDefault_ThrowsInvalidOperationException() - { - var options = new HtmxToolkitOptions(); - var defaultHtmxConfig = options.HtmxConfig; - - Assert.Multiple(() => - { - Assert.That(defaultHtmxConfig, Is.TypeOf()); - Assert.That( - () => options.UseHtmxV4(config => config.DefaultTimeout = 5000), - Throws.TypeOf() - .With.Message.EqualTo("HTMX has already been configured for version V2. Cannot reconfigure to V4.") - ); - }); - } - - [Test] public void AddHtmxToolkit_DependencyInjection_ExposesConfiguredOptions() { @@ -136,19 +117,17 @@ public void AddHtmxToolkit_DependencyInjection_ExposesConfiguredOptions() } [Test] - public void AddHtmxToolkit_WithoutConfiguration_ExposesDefaultOptions() + public void AddHtmxToolkit_WithoutSelectedVersion_FailsValidation() { var services = new ServiceCollection(); - services.AddHtmxToolkit(); + services.AddHtmxToolkit(options => options.IncludeAntiforgeryToken = false); using var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>().Value; + var options = provider.GetRequiredService>(); - Assert.Multiple(() => - { - Assert.That(options.TargetVersion, Is.EqualTo(HtmxTargetVersion.V2)); - Assert.That(options.HtmxConfig, Is.TypeOf()); - Assert.That(options.IncludeAntiforgeryToken, Is.True); - }); + Assert.That( + () => options.Value, + Throws.TypeOf() + .With.Message.EqualTo("The HTMX version is not selected. Call UseHtmxV1, UseHtmxV2, or UseHtmxV4 in AddHtmxToolkit.")); } } diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs index 55c45c2..ea5d36a 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs @@ -3,6 +3,7 @@ using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Ramstack.HtmxToolkit.Configuration; @@ -419,6 +420,21 @@ public void Reselect_SetsHeader() Assert.That(context.Response.Headers[HtmxResponseHeaderNames.Reselect], Is.EqualTo("#list")); } + [Test] + public void TriggerEvent_WithoutSelectedVersion_Throws() + { + var services = new ServiceCollection(); + services.AddOptions(); + + var context = new DefaultHttpContext { RequestServices = services.BuildServiceProvider() }; + context.Request.Headers[HtmxRequestHeaderNames.Request] = "true"; + + Assert.That( + () => context.Response.Htmx(r => r.TriggerEvent("notify")), + Throws.TypeOf() + .With.Message.EqualTo("The HTMX version is not selected. Call UseHtmxV1, UseHtmxV2, or UseHtmxV4 in AddHtmxToolkit.")); + } + [Test] public void TriggerEvent_SetsReceiveTrigger() { diff --git a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxConfigTagHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxConfigTagHelperTests.cs index 75ac172..e30ea54 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxConfigTagHelperTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxConfigTagHelperTests.cs @@ -16,7 +16,7 @@ public class HtmxConfigTagHelperTests public async Task ProcessAsync_RendersMetaTag() { var output = TestHelper.CreateTagHelperOutput(); - var helper = CreateHelper(new HtmxToolkitOptions()); + var helper = CreateHelper(new HtmxToolkitOptions().UseHtmxV2()); await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); @@ -37,7 +37,7 @@ public async Task ProcessAsync_RemovesHtmxConfigAttribute_FromMetaTag() }; var output = TestHelper.CreateTagHelperOutput("meta", attributes); - var helper = CreateHelper(new HtmxToolkitOptions()); + var helper = CreateHelper(new HtmxToolkitOptions().UseHtmxV2()); await helper.ProcessAsync(TestHelper.CreateTagHelperContext("meta", attributes), output); @@ -48,7 +48,7 @@ public async Task ProcessAsync_RemovesHtmxConfigAttribute_FromMetaTag() public async Task ProcessAsync_WithDefaults_SerializesEmptyHtmx2Object() { var output = TestHelper.CreateTagHelperOutput(); - var helper = CreateHelper(new HtmxToolkitOptions()); + var helper = CreateHelper(new HtmxToolkitOptions().UseHtmxV2()); await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); @@ -59,7 +59,7 @@ public async Task ProcessAsync_WithDefaults_SerializesEmptyHtmx2Object() public async Task ProcessAsync_UsesSingleQuotesForJsonAttribute() { var output = TestHelper.CreateTagHelperOutput(); - var helper = CreateHelper(new HtmxToolkitOptions()); + var helper = CreateHelper(new HtmxToolkitOptions().UseHtmxV2()); await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); @@ -394,7 +394,7 @@ public async Task ProcessAsync_AntiforgeryEnabledByDefault_RendersDataAttributes { var antiforgery = new StubAntiforgery(); var httpContext = new DefaultHttpContext(); - var options = new HtmxToolkitOptions(); + var options = new HtmxToolkitOptions().UseHtmxV2(); var helper = CreateHelper(options, antiforgery); helper.ViewContext = new ViewContext { HttpContext = httpContext }; @@ -418,7 +418,7 @@ public async Task ProcessAsync_AntiforgeryDisabled_DoesNotRequestOrRenderTokens( var antiforgery = new StubAntiforgery(); var output = TestHelper.CreateTagHelperOutput(); - await CreateHelper(new HtmxToolkitOptions { IncludeAntiforgeryToken = false }, antiforgery) + await CreateHelper(new HtmxToolkitOptions { IncludeAntiforgeryToken = false }.UseHtmxV2(), antiforgery) .ProcessAsync(TestHelper.CreateTagHelperContext(), output); Assert.Multiple(() => From 09622181c37b099aad062a6c109cf2240246e94a Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 3 Nov 2026 03:19:39 +0500 Subject: [PATCH 2/2] config: document the required HTMX version selection Select the version in every registration example and remove the statements about the HTMX 2.x default. Describe the startup and runtime errors in the version guide, the migration guide, and troubleshooting. --- README.md | 7 +++--- docs/articles/antiforgery.md | 2 +- docs/articles/choosing-version.md | 9 ++++---- docs/articles/configuration-v2.md | 2 +- docs/articles/configuration-v4.md | 2 +- docs/articles/configuration.md | 1 + docs/articles/forms-and-validation.md | 2 +- docs/articles/getting-started.md | 5 +++-- docs/articles/migrate-to-v2.md | 22 +++++++++++++------ docs/articles/minimal-apis.md | 2 +- docs/articles/mvc.md | 6 ++--- docs/articles/overview.md | 6 ++--- docs/articles/samples.md | 2 +- docs/articles/troubleshooting.md | 13 +++++++++-- docs/index.md | 2 +- .../snippets/configuration/RegisterToolkit.cs | 4 ++-- 16 files changed, 54 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 7c02d17..fa45a6a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ HtmxToolkit lets you do all of this from C# and Razor, without header strings or - 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. -HtmxToolkit runs on .NET 6 or later and supports HTMX 1.9.x, 2.x, and 4.x. HTMX 2.x is the default. +HtmxToolkit runs on .NET 6 or later and supports HTMX 1.9.x, 2.x, and 4.x. ## Getting started @@ -36,10 +36,11 @@ from a CDN or as a local file. ```csharp using Ramstack.HtmxToolkit.Hosting; -builder.Services.AddHtmxToolkit(); +builder.Services.AddHtmxToolkit(options => options.UseHtmxV2()); ``` -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). +Select the HTMX version your application loads: `UseHtmxV1`, `UseHtmxV2`, or `UseHtmxV4`. +The application does not start without it. See [Configuration](#configuration) for details. **3. Enable the Tag Helpers in `_ViewImports.cshtml`:** diff --git a/docs/articles/antiforgery.md b/docs/articles/antiforgery.md index a05fd62..c16e2ea 100644 --- a/docs/articles/antiforgery.md +++ b/docs/articles/antiforgery.md @@ -35,7 +35,7 @@ See [Static assets and deployment](static-assets.md) for script URLs, caching, a ## Enable server-side validation -Sending a token and validating it are separate steps. `AddHtmxToolkit()` configures toolkit services; +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. ### Razor Pages diff --git a/docs/articles/choosing-version.md b/docs/articles/choosing-version.md index 00086be..98ab554 100644 --- a/docs/articles/choosing-version.md +++ b/docs/articles/choosing-version.md @@ -3,7 +3,9 @@ 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. +The selection is required. If `AddHtmxToolkit` selects no version, the application fails to start. +If `AddHtmxToolkit` is not called, the Tag Helpers that depend on the version and `TriggerEvent` +throw `InvalidOperationException` when they run. ## Select a version @@ -38,7 +40,6 @@ trying to select a different version causes startup validation to fail. | Behavior | HTMX 1.9.x | HTMX 2.x | HTMX 4.x | |---------------------------|-------------------------------------------|-------------------------------------------|-------------------| -| Default when omitted | No | Yes | No | | Global config type | `HtmxV1Config` | `HtmxV2Config` | `HtmxV4Config` | | Per-request attribute | `hx-request` | `hx-request` | `hx-config` | | Triggering element header | `HX-Trigger` | `HX-Trigger` | `HX-Source` | @@ -70,8 +71,8 @@ Only settings you assign a value are included in the generated configuration. A For an existing application, select the version it already loads and review [Version compatibility](version-compatibility.md) before upgrading. 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. +tutorials without changes. 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.9.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-v2.md b/docs/articles/configuration-v2.md index e8f77e1..77e3d63 100644 --- a/docs/articles/configuration-v2.md +++ b/docs/articles/configuration-v2.md @@ -1,6 +1,6 @@ # HTMX 2.x configuration -HTMX 2.x is the default target. Calling `UseHtmxV2` is optional unless settings need to be changed: +Call `UseHtmxV2` when the application loads HTMX 2.x: ```csharp using Ramstack.HtmxToolkit; diff --git a/docs/articles/configuration-v4.md b/docs/articles/configuration-v4.md index 26a8150..a94247f 100644 --- a/docs/articles/configuration-v4.md +++ b/docs/articles/configuration-v4.md @@ -1,6 +1,6 @@ # HTMX 4.x configuration -Select V4 explicitly when the application loads HTMX 4.x: +Call `UseHtmxV4` when the application loads HTMX 4.x: ```csharp using Ramstack.HtmxToolkit; diff --git a/docs/articles/configuration.md b/docs/articles/configuration.md index 98825e3..cd6fc32 100644 --- a/docs/articles/configuration.md +++ b/docs/articles/configuration.md @@ -2,6 +2,7 @@ Application-wide settings are registered with `AddHtmxToolkit` and rendered into the page by ``. HtmxToolkit uses a different strongly typed configuration class for each supported HTMX major version. +Selecting the version is required; see [Choose an HTMX version](choosing-version.md). ## Configure HTMX diff --git a/docs/articles/forms-and-validation.md b/docs/articles/forms-and-validation.md index 3a82abc..98cadb7 100644 --- a/docs/articles/forms-and-validation.md +++ b/docs/articles/forms-and-validation.md @@ -192,7 +192,7 @@ Run the application as in Getting started and open options.UseHtmxV2()); var app = builder.Build(); @@ -41,7 +41,8 @@ app.MapRazorPages(); app.Run(); ``` -`AddHtmxToolkit()` selects HTMX 2.x by default. It registers toolkit options; it does not download HTMX. +`UseHtmxV2()` selects the HTMX major version that this tutorial loads. The selection is required: the application +does not start without it. `AddHtmxToolkit` 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. diff --git a/docs/articles/migrate-to-v2.md b/docs/articles/migrate-to-v2.md index 3801b75..9d43a74 100644 --- a/docs/articles/migrate-to-v2.md +++ b/docs/articles/migrate-to-v2.md @@ -16,8 +16,8 @@ step; see [Version compatibility](version-compatibility.md) for that. | Area | Change | Detected by | |---------------------------------------------------------------------|-------------------------------------------------------------------------------------|-------------------| -| [Registration](#register-the-toolkit-and-select-the-htmx-version) | Call `AddHtmxToolkit()` and select the HTMX version; HTMX 2.x is the default | **Silent** | -| [Global configuration](#move-global-configuration-to-code) | `` attributes move to `AddHtmxToolkit()` | **Silent** | +| [Registration](#register-the-toolkit-and-select-the-htmx-version) | Call `AddHtmxToolkit` and select the HTMX version | Runtime exception | +| [Global configuration](#move-global-configuration-to-code) | `` attributes move to `AddHtmxToolkit` | **Silent** | | [Antiforgery](#antiforgery-metadata-is-on-by-default) | Metadata on by default, moved out of `htmx.config`; token sent to own origin only | **Silent** | | [Toolkit script](#load-the-toolkit-script-as-a-static-asset) | Inline script helpers and `MapHtmxAntiforgeryScript` replaced by a static web asset | Build, **silent** | | [URL Tag Helper](#rename-the-url-tag-helper-attributes) | `hx-*` URL attributes become `asp-*`; `hx-host` and `hx-protocol` are removed | **Silent** | @@ -49,10 +49,10 @@ Add these namespaces where needed: ## Register the toolkit and select the HTMX version -Version 1.x had no registration and was written for HTMX 1.9.x. Version 2.0 registers its options with `AddHtmxToolkit()` -and generates markup for the selected HTMX version. **The default is HTMX 2.x.** +Version 1.x had no registration and was written for HTMX 1.9.x. Version 2.0 registers its options with `AddHtmxToolkit` +and generates markup for the selected HTMX version. **Selecting a version is required.** -If the application still loads HTMX 1.9.x, select it explicitly: +If the application still loads HTMX 1.9.x, select it: ```csharp using Ramstack.HtmxToolkit.Hosting; @@ -60,10 +60,14 @@ using Ramstack.HtmxToolkit.Hosting; builder.Services.AddHtmxToolkit(options => options.UseHtmxV1()); ``` -For HTMX 2.x, `builder.Services.AddHtmxToolkit()` is enough. The selected version controls the configuration +For HTMX 2.x or 4.x, call `UseHtmxV2` or `UseHtmxV4` instead. The selected version controls the configuration properties available in code and the attributes generated by the Tag Helpers. See [Choose an HTMX version](choosing-version.md). +If `AddHtmxToolkit` selects no version, the application fails to start. If `AddHtmxToolkit` is not called, +the application starts, but the Tag Helpers that depend on the version, such as ``, and `TriggerEvent` +throw `InvalidOperationException` when they run. + ## Move global configuration to code In 1.x, `` and `` accepted HTMX settings as attributes. @@ -134,7 +138,11 @@ or [HTMX 4.x](configuration-v4.md) configuration. > disable the metadata: > > ```csharp -> builder.Services.AddHtmxToolkit(options => options.IncludeAntiforgeryToken = false); +> builder.Services.AddHtmxToolkit(options => +> { +> options.IncludeAntiforgeryToken = false; +> options.UseHtmxV2(); +> }); > ``` The metadata also moved. In 1.x it was an `antiForgery` object inside the configuration JSON, and the script diff --git a/docs/articles/minimal-apis.md b/docs/articles/minimal-apis.md index 65475ae..510c312 100644 --- a/docs/articles/minimal-apis.md +++ b/docs/articles/minimal-apis.md @@ -25,7 +25,7 @@ using Ramstack.HtmxToolkit; using Ramstack.HtmxToolkit.Hosting; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddHtmxToolkit(); +builder.Services.AddHtmxToolkit(options => options.UseHtmxV2()); builder.Services.AddAntiforgery(options => options.HeaderName = "RequestVerificationToken"); var app = builder.Build(); diff --git a/docs/articles/mvc.md b/docs/articles/mvc.md index cbd9454..859c4ad 100644 --- a/docs/articles/mvc.md +++ b/docs/articles/mvc.md @@ -1,6 +1,6 @@ # Use HtmxToolkit with MVC -This example uses ASP.NET Core MVC on .NET 10 and the default HTMX 2.x target. +This example uses ASP.NET Core MVC on .NET 10 and HTMX 2.x. It loads product details into a partial view, then edits the product name with server validation. The request and response APIs are the same as in Razor Pages; MVC uses controller actions and `PartialView()` instead of page handlers and `Partial()`. @@ -26,7 +26,7 @@ builder.Services.AddControllersWithViews(options => { options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); }); -builder.Services.AddHtmxToolkit(); +builder.Services.AddHtmxToolkit(options => options.UseHtmxV2()); builder.Services.AddSingleton(); var app = builder.Build(); @@ -38,7 +38,7 @@ app.Run(); ``` The antiforgery filter validates the POST action used below. -`AddHtmxToolkit()` does not install that filter for you. +`AddHtmxToolkit` does not install that filter for you. Add these lines to the existing `Views/_ViewImports.cshtml`: diff --git a/docs/articles/overview.md b/docs/articles/overview.md index c2aab3b..6dc3183 100644 --- a/docs/articles/overview.md +++ b/docs/articles/overview.md @@ -96,14 +96,14 @@ registration, and layout needed to run it. | 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 | +| Trigger client events, use Tag Helpers, or configure HTMX | Select a version in `AddHtmxToolkit`; render `` in Razor | | 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.9.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 +Select the 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 diff --git a/docs/articles/samples.md b/docs/articles/samples.md index 2681345..f8b3dc8 100644 --- a/docs/articles/samples.md +++ b/docs/articles/samples.md @@ -16,7 +16,7 @@ dotnet run --project samples/Ramstack.HtmxToolkit.Demo --no-launch-profile -- -- Open . This command uses HTTP for local development, so a development HTTPS certificate is not needed. The demo loads HTMX from a CDN and requires a network connection for that script. -The demo currently selects HTMX 4.x. The introductory tutorials use HTMX 2.x, the toolkit default. +The demo currently selects HTMX 4.x. The introductory tutorials use HTMX 2.x. To try another supported version, change both `UseHtmxV4(...)` in `Program.cs` and the HTMX script in `Pages/Shared/_Layout.cshtml`. Review [Version compatibility](version-compatibility.md) before changing version-specific options such as `NoSwap`. diff --git a/docs/articles/troubleshooting.md b/docs/articles/troubleshooting.md index 94db036..7d12d4c 100644 --- a/docs/articles/troubleshooting.md +++ b/docs/articles/troubleshooting.md @@ -200,13 +200,22 @@ Check the response status. HTMX cannot process `HX-Redirect` or other `HX-*` hea because the browser follows that redirect internally. Return status 200 with `HX-Redirect` for the HTMX request, and use an ordinary ASP.NET Core redirect only for the non-HTMX path. +## The HTMX version is not selected + +**Symptom:** The application fails to start with `OptionsValidationException`, or a page or a `TriggerEvent` call +throws `InvalidOperationException`, with the message `The HTMX version is not selected`. + +**Check:** Call `AddHtmxToolkit` in `Program.cs` and select the major version that the browser loads: + +```csharp +builder.Services.AddHtmxToolkit(options => options.UseHtmxV2()); +``` + ## The wrong per-request attribute is generated HTMX 1.9.x and 2.x receive `hx-request`; HTMX 4.x receives `hx-config`. Check `UseHtmxV1`, `UseHtmxV2`, or `UseHtmxV4` in service registration and verify that the browser loads the same major version. -If no version is selected, HtmxToolkit uses V2. - ## Request metadata is null Some properties are version-specific: diff --git a/docs/index.md b/docs/index.md index 9f67e61..8218781 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,7 +32,7 @@ Choose your starting point: ## Supported environments The package targets .NET 6 and runs on .NET 6 or later. -It supports HTMX 1.9.x, HTMX 2.x, and HTMX 4.x; HTMX 2.x is the default target. +It supports HTMX 1.9.x, HTMX 2.x, and HTMX 4.x; the application selects the version it loads. > [!IMPORTANT] > HtmxToolkit does not bundle HTMX. Add the matching HTMX client release to the application separately. diff --git a/docs/snippets/configuration/RegisterToolkit.cs b/docs/snippets/configuration/RegisterToolkit.cs index 5b52384..62f0c6e 100644 --- a/docs/snippets/configuration/RegisterToolkit.cs +++ b/docs/snippets/configuration/RegisterToolkit.cs @@ -4,8 +4,8 @@ builder.Services.AddRazorPages(); -// HTMX 2.x is the default. Select the version that your pages load, -// and set only the HTMX options that should differ from the HTMX defaults. +// Select the version that your pages load, and set only +// the HTMX options that should differ from the HTMX defaults. builder.Services.AddHtmxToolkit(options => { options.UseHtmxV4(config =>