From 1e450d13690526bb4ec1b9a1a8872d53a8433117 Mon Sep 17 00:00:00 2001 From: rameel Date: Sun, 6 Sep 2026 01:07:41 +0500 Subject: [PATCH 1/3] Add explicit attribute inheritance support for HTMX 4 --- .../TagHelpers/HtmxHeaderTagHelper.cs | 35 +++++++++++-- .../TagHelpers/HtmxRequestTagHelper.cs | 52 ++++++++++++++++--- .../TagHelpers/HtmxValsTagHelper.cs | 31 +++++++++-- 3 files changed, 104 insertions(+), 14 deletions(-) diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs index 359bfdd..6190d6d 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs @@ -2,8 +2,10 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Options; using Ramstack.HtmxToolkit.Collections; +using Ramstack.HtmxToolkit.Configuration; using Ramstack.HtmxToolkit.Serialization; namespace Ramstack.HtmxToolkit.TagHelpers; @@ -13,17 +15,33 @@ namespace Ramstack.HtmxToolkit.TagHelpers; /// /// /// -/// hx-headers is inherited and can be placed on a parent element. +/// HTMX 1.x and 2.x merge-inherit hx-headers from parent elements. +/// +/// HTMX 4.x requires either the explicit inheritance modifier or the global +/// option for inheritance. +/// /// A child declaration of a header overrides a parent declaration. /// /// +[HtmlTargetElement(Attributes = InheritedAttributeName)] [HtmlTargetElement(Attributes = HeadersDictionaryName)] [HtmlTargetElement(Attributes = HeadersPrefix + "*")] -public sealed class HtmxHeaderTagHelper : TagHelper +public sealed class HtmxHeaderTagHelper(IOptions options) : TagHelper { + private const string InheritedAttributeName = "hx-headers-inherited"; private const string HeadersPrefix = "hx-header-"; private const string HeadersDictionaryName = "hx-all-headers"; + /// + /// Gets or sets a value indicating whether hx-headers is explicitly inherited. + /// + /// + /// HTMX 1.x and 2.x merge-inherit headers without a modifier. + /// HTMX 4.x emits the inherited modifier when this property is . + /// + [HtmlAttributeName(InheritedAttributeName)] + public bool Inherited { get; set; } + /// /// Gets or sets the hx-headers attribute values. /// @@ -39,11 +57,20 @@ public override Task ProcessAsync(TagHelperContext context, TagHelperOutput outp { if (Headers is { Count: > 0 }) { + var name = "hx-headers"; + if (Inherited && options.Value.TargetVersion == HtmxTargetVersion.V4) + { + if (options.Value.HtmxConfig is HtmxV4Config config) + name = string.IsNullOrEmpty(config.MetaCharacter) + ? "hx-headers:inherited" + : $"hx-headers{config.MetaCharacter}inherited"; + } + var info = HtmxDictionaryJsonSerializerContext.Default.IDictionaryStringString; - var headers = new HtmlString(JsonSerializer.Serialize(Headers, info)); + var json = JsonSerializer.Serialize(Headers, info); output.Attributes.SetAttribute( - new TagHelperAttribute("hx-headers", headers, HtmlAttributeValueStyle.SingleQuotes)); + new TagHelperAttribute(name, new HtmlString(json), HtmlAttributeValueStyle.SingleQuotes)); } return Task.CompletedTask; diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs index e35919e..ce67c78 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs @@ -14,8 +14,12 @@ namespace Ramstack.HtmxToolkit.TagHelpers; /// /// /// HTMX 1.x and 2.x use the merge-inherited hx-request attribute. -/// HTMX 4.x uses the hx-config attribute. +/// +/// HTMX 4.x uses hx-config and requires either the explicit inheritance modifier +/// or the global option for inheritance. +/// /// +[HtmlTargetElement(Attributes = RequestInheritedAttributeName)] [HtmlTargetElement(Attributes = RequestTimeoutAttributeName)] [HtmlTargetElement(Attributes = RequestCredentialsAttributeName)] [HtmlTargetElement(Attributes = RequestNoHeadersAttributeName)] @@ -26,6 +30,7 @@ namespace Ramstack.HtmxToolkit.TagHelpers; [HtmlTargetElement(Attributes = RequestValidateAttributeName)] public sealed class HtmxRequestTagHelper(IOptions options) : TagHelper { + private const string RequestInheritedAttributeName = "hx-request-inherited"; private const string RequestTimeoutAttributeName = "hx-request-timeout"; private const string RequestCredentialsAttributeName = "hx-request-credentials"; private const string RequestNoHeadersAttributeName = "hx-request-no-headers"; @@ -37,6 +42,16 @@ public sealed class HtmxRequestTagHelper(IOptions options) : private readonly HtmxRequestData _request = new(); + /// + /// Gets or sets a value indicating whether the generated attribute is explicitly inherited. + /// + /// + /// HTMX 1.x and 2.x merge-inherit request configuration without a modifier. + /// HTMX 4.x emits the inherited modifier when this property is . + /// + [HtmlAttributeName(RequestInheritedAttributeName)] + public bool Inherited { get; set; } + /// /// Gets or sets the timeout for the request in milliseconds. /// @@ -143,22 +158,37 @@ public bool? Validate /// public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { - var targetVersion = options.Value.TargetVersion; - var request = targetVersion == HtmxTargetVersion.V4 + var json = options.Value.TargetVersion == HtmxTargetVersion.V4 ? JsonSerializer.Serialize(new HtmxRequestDataV4(_request), HtmxRequestJsonSerializerContext.Default.HtmxRequestDataV4) : JsonSerializer.Serialize(new HtmxRequestDataPrior(_request), HtmxRequestJsonSerializerContext.Default.HtmxRequestDataPrior); - if (request != "{}") + if (json != "{}") { - var attributeName = targetVersion == HtmxTargetVersion.V4 ? "hx-config" : "hx-request"; + var name = "hx-request"; + + if (options.Value.TargetVersion == HtmxTargetVersion.V4) + { + if (Inherited) + { + if (options.Value.HtmxConfig is HtmxV4Config config) + name = string.IsNullOrEmpty(config.MetaCharacter) + ? "hx-config:inherited" + : $"hx-config{config.MetaCharacter}inherited"; + } + else + { + name = "hx-config"; + } + } + output.Attributes.SetAttribute( - new TagHelperAttribute(attributeName, new HtmlString(request), HtmlAttributeValueStyle.SingleQuotes)); + new TagHelperAttribute(name, new HtmlString(json), HtmlAttributeValueStyle.SingleQuotes)); } return Task.CompletedTask; } - #region Inner types + #region Inner type: HtmxRequestData /// /// Stores the request configuration shared by all supported HTMX versions. @@ -190,6 +220,10 @@ internal sealed class HtmxRequestData public bool? Validate { get; set; } } + #endregion + + #region Inner type: HtmxRequestDataPrior + /// /// Projects request configuration into the hx-request contract used by HTMX 1.x and 2.x. /// @@ -215,6 +249,10 @@ internal readonly struct HtmxRequestDataPrior(HtmxRequestData data) public bool? NoHeaders => data.NoHeaders; } + #endregion + + #region Inner type: HtmxRequestDataV4 + /// /// Projects request configuration into the hx-config contract used by HTMX 4.x. /// diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxValsTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxValsTagHelper.cs index 8723f8c..09b0342 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxValsTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxValsTagHelper.cs @@ -2,8 +2,10 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Options; using Ramstack.HtmxToolkit.Collections; +using Ramstack.HtmxToolkit.Configuration; using Ramstack.HtmxToolkit.Serialization; namespace Ramstack.HtmxToolkit.TagHelpers; @@ -13,17 +15,33 @@ namespace Ramstack.HtmxToolkit.TagHelpers; /// /// /// -/// hx-vals is inherited and can be placed on a parent element. +/// HTMX 1.x and 2.x merge-inherit hx-vals from parent elements. +/// +/// HTMX 4.x requires either the explicit inheritance modifier or the global +/// option for inheritance. +/// /// A child declaration of a value overrides a parent declaration. /// /// +[HtmlTargetElement(Attributes = InheritedAttributeName)] [HtmlTargetElement(Attributes = ValuesDictionaryName)] [HtmlTargetElement(Attributes = ValuesPrefix + "*")] -public sealed class HtmxValsTagHelper : TagHelper +public sealed class HtmxValsTagHelper(IOptions options) : TagHelper { + private const string InheritedAttributeName = "hx-vals-inherited"; private const string ValuesPrefix = "hx-val-"; private const string ValuesDictionaryName = "hx-all-vals"; + /// + /// Gets or sets a value indicating whether hx-vals is explicitly inherited. + /// + /// + /// HTMX 1.x and 2.x merge-inherit values without a modifier. + /// HTMX 4.x emits the inherited modifier when this property is . + /// + [HtmlAttributeName(InheritedAttributeName)] + public bool Inherited { get; set; } + /// /// Gets or sets the hx-vals attribute values. /// @@ -39,9 +57,16 @@ public override Task ProcessAsync(TagHelperContext context, TagHelperOutput outp { if (Values is { Count: > 0 } values) { + var name = "hx-vals"; + if (Inherited && options.Value.TargetVersion == HtmxTargetVersion.V4) + if (options.Value.HtmxConfig is HtmxV4Config config) + name = string.IsNullOrEmpty(config.MetaCharacter) + ? "hx-vals:inherited" + : $"hx-vals{config.MetaCharacter}inherited"; + var info = HtmxDictionaryJsonSerializerContext.Default.IDictionaryStringString; var json = JsonSerializer.Serialize(values, info); - var attribute = new TagHelperAttribute("hx-vals", new HtmlString(json), HtmlAttributeValueStyle.SingleQuotes); + var attribute = new TagHelperAttribute(name, new HtmlString(json), HtmlAttributeValueStyle.SingleQuotes); output.Attributes.SetAttribute(attribute); } From 0bf2744e8e81c61ea79398caa8651caa0dc6dc9c Mon Sep 17 00:00:00 2001 From: rameel Date: Sun, 6 Sep 2026 01:39:52 +0500 Subject: [PATCH 2/3] Add tests --- .../PendingEventsTests.cs | 2 +- .../TagHelpers/HtmxHeaderTagHelperTests.cs | 134 ++++++++++++++---- .../TagHelpers/HtmxRequestTagHelperTests.cs | 107 +++++++++++++- .../TagHelpers/HtmxValsTagHelperTests.cs | 123 +++++++++++++--- 4 files changed, 320 insertions(+), 46 deletions(-) diff --git a/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs b/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs index 8c5f069..cc82494 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/PendingEventsTests.cs @@ -60,7 +60,7 @@ public void AddEvents_TracksTimingsIndependently() [TestCase(HtmxTargetVersion.V1)] [TestCase(HtmxTargetVersion.V2)] - public void AddEvents_LegacyVersions_WriteEachTimingToItsOwnHeader(HtmxTargetVersion targetVersion) + public void AddEvents_PriorVersions_WriteEachTimingToItsOwnHeader(HtmxTargetVersion targetVersion) { var context = TestHelper.CreateHtmxRequestContext(targetVersion); var pending = PendingEvents.GetOrCreate(context.Response); diff --git a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxHeaderTagHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxHeaderTagHelperTests.cs index 77b4dc7..d0bd639 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxHeaderTagHelperTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxHeaderTagHelperTests.cs @@ -1,5 +1,8 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Options; + +using Ramstack.HtmxToolkit.Configuration; namespace Ramstack.HtmxToolkit.Tests.TagHelpers; @@ -10,14 +13,9 @@ public class HtmxHeaderTagHelperTests public async Task ProcessAsync_SerializesHeaders() { var output = TestHelper.CreateTagHelperOutput(); - var helper = new HtmxHeaderTagHelper - { - Headers = - { - ["X-Requested-With"] = "XMLHttpRequest", - ["X-Custom"] = "value's" - } - }; + var helper = CreateHelper(); + helper.Headers["X-Requested-With"] = "XMLHttpRequest"; + helper.Headers["X-Custom"] = "value's"; await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); var attribute = output.Attributes["hx-headers"]; @@ -34,14 +32,9 @@ public async Task ProcessAsync_SerializesHeaders() [Test] public void Headers_TreatsNamesAsCaseInsensitive() { - var helper = new HtmxHeaderTagHelper - { - Headers = - { - ["X-Custom"] = "first", - ["x-custom"] = "second" - } - }; + var helper = CreateHelper(); + helper.Headers["X-Custom"] = "first"; + helper.Headers["x-custom"] = "second"; Assert.That(helper.Headers, Has.Count.EqualTo(1)); Assert.That(helper.Headers["X-CUSTOM"], Is.EqualTo("second")); @@ -51,13 +44,8 @@ public void Headers_TreatsNamesAsCaseInsensitive() public async Task ProcessAsync_UsesSingleQuotesForJsonAttribute() { var output = TestHelper.CreateTagHelperOutput(); - var helper = new HtmxHeaderTagHelper - { - Headers = - { - ["X-Custom"] = "value" - } - }; + var helper = CreateHelper(); + helper.Headers["X-Custom"] = "value"; await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); @@ -68,11 +56,109 @@ public async Task ProcessAsync_UsesSingleQuotesForJsonAttribute() public async Task ProcessAsync_SerializesEmptyDictionary() { var output = TestHelper.CreateTagHelperOutput(); - var helper = new HtmxHeaderTagHelper(); + var helper = CreateHelper(); await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); var attribute = output.Attributes["hx-headers"]; Assert.That(attribute, Is.Null); } + + [Test] + public async Task ProcessAsync_Htmx4_Inherited_UsesInheritedAttribute() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + + helper.Inherited = true; + helper.Headers["X-Custom"] = "value"; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + var attribute = output.Attributes["hx-headers:inherited"]; + + Assert.That(attribute.Value.ToString(), Is.EqualTo("{\"X-Custom\":\"value\"}")); + Assert.That(output.Attributes["hx-headers"], Is.Null); + } + + [Test] + public async Task ProcessAsync_Htmx4_Inherited_UsesConfiguredMetaCharacter() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4, "-"); + + helper.Inherited = true; + helper.Headers["X-Custom"] = "value"; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + var attribute = output.Attributes["hx-headers-inherited"]; + + Assert.That(attribute!.Value.ToString(), Is.EqualTo("{\"X-Custom\":\"value\"}")); + Assert.That(output.Attributes["hx-headers:inherited"], Is.Null); + } + + [Test] + public async Task ProcessAsync_Htmx4_NotInherited_UsesOrdinaryAttribute() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + helper.Headers["X-Custom"] = "value"; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + var attribute = output.Attributes["hx-headers"]; + + Assert.That(attribute!.Value.ToString(), Is.EqualTo("{\"X-Custom\":\"value\"}")); + Assert.That(output.Attributes["hx-headers:inherited"], Is.Null); + } + + [Test] + public async Task ProcessAsync_Htmx4_Inherited_WithEmptyDictionary_OmitsAttribute() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + helper.Inherited = true; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + Assert.That(output.Attributes, Is.Empty); + } + + [TestCase(HtmxTargetVersion.V1)] + [TestCase(HtmxTargetVersion.V2)] + public async Task ProcessAsync_Htmx1_Htmx2_Inherited_UsesOrdinaryAttribute(HtmxTargetVersion version) + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(version); + + helper.Inherited = true; + helper.Headers["X-Custom"] = "value"; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + var attribute = output.Attributes["hx-headers"]; + + Assert.That(attribute!.Value.ToString(), Is.EqualTo("{\"X-Custom\":\"value\"}")); + Assert.That(output.Attributes["hx-headers:inherited"], Is.Null); + Assert.That(output.Attributes["hx-inherit"], Is.Null); + } + + private static HtmxHeaderTagHelper CreateHelper(HtmxTargetVersion version = HtmxTargetVersion.V2, string? metachar = null) + { + var options = new HtmxToolkitOptions(); + + switch (version) + { + case HtmxTargetVersion.V1: + options.UseHtmxV1(); + break; + case HtmxTargetVersion.V2: + options.UseHtmxV2(); + break; + case HtmxTargetVersion.V4: + options.UseHtmxV4(config => config.MetaCharacter = metachar); + break; + default: + throw new ArgumentOutOfRangeException(nameof(version)); + } + + return new HtmxHeaderTagHelper(Options.Create(options)); + } } diff --git a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs index 07d5123..da19db9 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxRequestTagHelperTests.cs @@ -12,7 +12,7 @@ public class HtmxRequestTagHelperTests [Test] [TestCase(HtmxTargetVersion.V1)] [TestCase(HtmxTargetVersion.V2)] - public async Task ProcessAsync_SerializesLegacyRequestConfiguration(HtmxTargetVersion version) + public async Task ProcessAsync_Serializes_Htmx1_Htmx2_RequestConfiguration(HtmxTargetVersion version) { var output = TestHelper.CreateTagHelperOutput(); var helper = CreateHelper(version); @@ -67,7 +67,7 @@ public async Task ProcessAsync_SerializesNoHeadersWhenTrue() } [Test] - public async Task ProcessAsync_OmitsUnsetLegacyProperties() + public async Task ProcessAsync_Htmx1_Htmx2_OmitsUnsetProperties() { var output = TestHelper.CreateTagHelperOutput(); var helper = CreateHelper(HtmxTargetVersion.V2); @@ -137,7 +137,7 @@ public async Task ProcessAsync_SerializesHtmx4CredentialsMode(HtmxRequestCredent } [Test] - public async Task ProcessAsync_OmitsHtmx4OnlyCredentialsModeForLegacyVersions() + public async Task ProcessAsync_OmitsHtmx4OnlyCredentialsModeForPriorVersions() { var output = TestHelper.CreateTagHelperOutput(); var helper = CreateHelper(HtmxTargetVersion.V2); @@ -176,7 +176,100 @@ public async Task ProcessAsync_OmitsUnsetConfiguration() Assert.That(output.Attributes["hx-request"], Is.Null); } - private static HtmxRequestTagHelper CreateHelper(HtmxTargetVersion version) + [Test] + public async Task ProcessAsync_Htmx4_Inherited_UsesInheritedAttribute() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + + helper.Inherited = true; + helper.Timeout = 500; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + var attribute = output.Attributes["hx-config:inherited"]; + + Assert.That(attribute.Value.ToString(), Is.EqualTo("{\"timeout\":500}")); + Assert.That(output.Attributes["hx-config"], Is.Null); + } + + [Test] + public async Task ProcessAsync_Htmx4_Inherited_UsesConfiguredMetaCharacter() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4, "-"); + + helper.Inherited = true; + helper.Timeout = 500; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + var attribute = output.Attributes["hx-config-inherited"]; + + Assert.That(attribute!.Value.ToString(), Is.EqualTo("{\"timeout\":500}")); + Assert.That(output.Attributes["hx-config:inherited"], Is.Null); + } + + [Test] + public async Task ProcessAsync_Htmx4_Inherited_WithImplicitInheritance_StillUsesInheritedAttribute() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4, implicitInheritance: true); + + helper.Inherited = true; + helper.Timeout = 500; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + var attribute = output.Attributes["hx-config:inherited"]; + + Assert.That(attribute!.Value.ToString(), Is.EqualTo("{\"timeout\":500}")); + Assert.That(output.Attributes["hx-config"], Is.Null); + } + + [Test] + public async Task ProcessAsync_Htmx4NotInherited_UsesOrdinaryAttribute() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + helper.Timeout = 500; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + var attribute = output.Attributes["hx-config"]; + + Assert.That(attribute!.Value.ToString(), Is.EqualTo("{\"timeout\":500}")); + Assert.That(output.Attributes["hx-config:inherited"], Is.Null); + } + + [Test] + public async Task ProcessAsync_Inherited_WithEmptyConfiguration_OmitsAttribute() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(HtmxTargetVersion.V4); + helper.Inherited = true; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + Assert.That(output.Attributes, Is.Empty); + } + + [TestCase(HtmxTargetVersion.V1)] + [TestCase(HtmxTargetVersion.V2)] + public async Task ProcessAsync_Htmx1_Htmx2_Inherited_UsesOrdinaryAttribute(HtmxTargetVersion version) + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = CreateHelper(version); + + helper.Inherited = true; + helper.Timeout = 500; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + var attribute = output.Attributes["hx-request"]; + + Assert.That(attribute!.Value.ToString(), Is.EqualTo("{\"timeout\":500}")); + Assert.That(output.Attributes["hx-request:inherited"], Is.Null); + Assert.That(output.Attributes["hx-inherit"], Is.Null); + } + + private static HtmxRequestTagHelper CreateHelper(HtmxTargetVersion version, string? metachar = null, bool? implicitInheritance = null) { var options = new HtmxToolkitOptions(); @@ -189,7 +282,11 @@ private static HtmxRequestTagHelper CreateHelper(HtmxTargetVersion version) options.UseHtmxV2(); break; case HtmxTargetVersion.V4: - options.UseHtmxV4(); + options.UseHtmxV4(config => + { + config.MetaCharacter = metachar; + config.ImplicitInheritance = implicitInheritance; + }); break; default: throw new ArgumentOutOfRangeException(nameof(version)); diff --git a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxValsTagHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxValsTagHelperTests.cs index 1211e25..8df9878 100644 --- a/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxValsTagHelperTests.cs +++ b/tests/Ramstack.HtmxToolkit.Tests/TagHelpers/HtmxValsTagHelperTests.cs @@ -1,5 +1,8 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.Extensions.Options; + +using Ramstack.HtmxToolkit.Configuration; namespace Ramstack.HtmxToolkit.Tests.TagHelpers; @@ -10,14 +13,9 @@ public class HtmxValsTagHelperTests public async Task ProcessAsync_SerializesValues() { var output = TestHelper.CreateTagHelperOutput(); - var helper = new HtmxValsTagHelper - { - Values = - { - ["категория"] = "Детские книги '