From a708173d46d117c7e366a255cb2c49b8f4af77b7 Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 3 Nov 2026 01:10:23 +0500 Subject: [PATCH] tag-helpers: make enum and boolean request options non-nullable Razor resolves a bare member name only for a non-nullable enum and allows an attribute without a value only for a non-nullable bool, so hx-request-cache="NoCache" failed with CS0103 and hx-request-validate without a value failed with RZ2008. Change Credentials, NoHeaders, Cache, Redirect, and Validate to non-nullable properties over the existing nullable storage. Razor calls a setter only for an attribute present in the markup, so an omitted attribute still leaves the option out of the JSON, while an explicit SameOrigin, Default, Follow, or false is still rendered. These attributes no longer accept a nullable value, such as @(condition ? HtmxRequestCache.NoCache : null), or a qualified name without @, such as HtmxRequestCache.NoCache. @HtmxRequestCache.NoCache keeps working. --- docs/articles/tag-helpers.md | 11 +++- .../Pages/Examples/RequestOptions.cshtml | 4 +- .../TagHelpers/HtmxRequestTagHelper.cs | 62 +++++++++++++------ .../TagHelpers/HtmxRequestTagHelperTests.cs | 24 ++++--- 4 files changed, 71 insertions(+), 30 deletions(-) diff --git a/docs/articles/tag-helpers.md b/docs/articles/tag-helpers.md index 8ec994d..04fc13d 100644 --- a/docs/articles/tag-helpers.md +++ b/docs/articles/tag-helpers.md @@ -245,7 +245,7 @@ Use `hx-request-*` attributes rather than hand-writing version-specific JSON: asp-page="/Reports" asp-page-handler="Preview" hx-request-timeout="2000" - hx-request-credentials="@HtmxRequestCredentials.SameOrigin" + hx-request-credentials="SameOrigin" hx-target="#preview"> Generate preview @@ -275,6 +275,11 @@ The output depends on the configured HTMX target: Unsupported properties are omitted from the generated JSON. In particular, `HtmxRequestCredentials.Omit` cannot be represented by HTMX 1.9.x or 2.x and is omitted for those targets. +Enum attributes take a member name, such as `hx-request-credentials="Include"`. A C# expression starts with `@`, +for example `hx-request-credentials="@credentials"`; `HtmxRequestCredentials.Include` without `@` does not compile. +`hx-request-no-headers` and `hx-request-validate` can be written without a value, which means `true`. +To leave an option unset, omit its attribute; these attributes do not accept `null`. + An element with `hx-request-*` attributes cannot also have a hand-written `hx-request` for HTMX 1.9.x and 2.x or `hx-config` for HTMX 4.x, with or without modifiers or the `data-hx-` prefix. The Tag Helper throws `InvalidOperationException` rather than lose one of the values. @@ -283,8 +288,8 @@ Use `HtmxRequestCache` and `HtmxRequestRedirect` for the HTMX 4.x Fetch options: ```razor ``` diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RequestOptions.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RequestOptions.cshtml index 0617195..8d019af 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RequestOptions.cshtml +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/RequestOptions.cshtml @@ -22,8 +22,8 @@ asp-page="/Examples/RequestOptions" asp-page-handler="Preview" hx-request-timeout="2000" - hx-request-cache="@HtmxRequestCache.NoCache" - hx-request-redirect="@HtmxRequestRedirect.Error" + hx-request-cache="NoCache" + hx-request-redirect="Error" hx-target="#preview-result"> Generate report preview diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs index d6c46bf..5d24703 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs @@ -111,11 +111,15 @@ public int? Timeout /// In HTMX 4.x, this property maps to the string credentials option /// of hx-config. /// + /// + /// If the property has not been set, the option is not rendered, HTMX uses its default value, + /// and the property returns . + /// /// [HtmlAttributeName(RequestCredentialsAttributeName)] - public HtmxRequestCredentials? Credentials + public HtmxRequestCredentials Credentials { - get => _request.Credentials; + get => _request.Credentials.GetValueOrDefault(); set => _request.Credentials = value; } @@ -127,33 +131,49 @@ public HtmxRequestCredentials? Credentials /// Supported only in HTMX 1.9.x and 2.x. /// Headers added by the browser are unaffected. /// The Toolkit script also omits the HX-Request-Type header. + /// + /// If the property has not been set, the option is not rendered, HTMX uses its default value, + /// and the property returns . + /// /// [HtmlAttributeName(RequestNoHeadersAttributeName)] - public bool? NoHeaders + public bool NoHeaders { - get => _request.NoHeaders; + get => _request.NoHeaders.GetValueOrDefault(); set => _request.NoHeaders = value; } /// /// Gets or sets the Fetch cache mode for the request. /// - /// Supported only in HTMX 4.x. + /// + /// Supported only in HTMX 4.x. + /// + /// If the property has not been set, the option is not rendered, HTMX uses its default value, + /// and the property returns . + /// + /// [HtmlAttributeName(RequestCacheAttributeName)] - public HtmxRequestCache? Cache + public HtmxRequestCache Cache { - get => _request.Cache; + get => _request.Cache.GetValueOrDefault(); set => _request.Cache = value; } /// /// Gets or sets the Fetch redirect mode for the request. /// - /// Supported only in HTMX 4.x. + /// + /// Supported only in HTMX 4.x. + /// + /// If the property has not been set, the option is not rendered, HTMX uses its default value, + /// and the property returns . + /// + /// [HtmlAttributeName(RequestRedirectAttributeName)] - public HtmxRequestRedirect? Redirect + public HtmxRequestRedirect Redirect { - get => _request.Redirect; + get => _request.Redirect.GetValueOrDefault(); set => _request.Redirect = value; } @@ -194,11 +214,17 @@ public string? Integrity /// /// Gets or sets a value indicating whether the form is validated before submission. /// - /// Supported only in HTMX 4.x. + /// + /// Supported only in HTMX 4.x. + /// + /// If the property has not been set, the option is not rendered, HTMX uses its default value, + /// and the property returns . + /// + /// [HtmlAttributeName(RequestValidateAttributeName)] - public bool? Validate + public bool Validate { - get => _request.Validate; + get => _request.Validate.GetValueOrDefault(); set => _request.Validate = value; } @@ -240,16 +266,16 @@ internal sealed class HtmxRequestData /// public int? Timeout { get; set; } - /// + /// public HtmxRequestCredentials? Credentials { get; set; } - /// + /// public bool? NoHeaders { get; set; } - /// + /// public HtmxRequestCache? Cache { get; set; } - /// + /// public HtmxRequestRedirect? Redirect { get; set; } /// @@ -258,7 +284,7 @@ internal sealed class HtmxRequestData /// public string? Integrity { get; set; } - /// + /// public bool? Validate { get; set; } } diff --git a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs index 1c2b8dd..f25921f 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs @@ -169,20 +169,30 @@ public async Task ProcessAsync_SerializesHtmx4RedirectMode(HtmxRequestRedirect r Assert.That(output.Attributes["hx-request"], Is.Null); } + [TestCase(HtmxTargetVersion.V1, "hx-request")] + [TestCase(HtmxTargetVersion.V2, "hx-request")] + [TestCase(HtmxTargetVersion.V4, "hx-config")] + public async Task ProcessAsync_OmitsUnsetOptions(HtmxTargetVersion version, string attributeName) + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(version); + helper.Timeout = 500; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + Assert.That(output.Attributes[attributeName]!.Value.ToString(), Is.EqualTo("{\"timeout\":500}")); + } + [Test] - public async Task ProcessAsync_Htmx4_OmitsClearedCacheAndRedirectModes() + public async Task ProcessAsync_Htmx4_SerializesValidateWhenFalse() { var output = TestHelper.CreateTagHelperOutput(); var helper = CreateHelper(HtmxTargetVersion.V4); - helper.Timeout = 500; - helper.Cache = HtmxRequestCache.Default; - helper.Redirect = HtmxRequestRedirect.Follow; - helper.Cache = null; - helper.Redirect = null; + helper.Validate = false; await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); - Assert.That(output.Attributes["hx-config"]!.Value.ToString(), Is.EqualTo("{\"timeout\":500}")); + Assert.That(output.Attributes["hx-config"]!.Value.ToString(), Is.EqualTo("{\"validate\":false}")); } [Test]