diff --git a/README.md b/README.md index d92a5e5..c1bd07f 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,31 @@ With HTMX 4.x selected, `HtmxRequestTagHelper` generates `hx-config` instead. 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. +### Attribute Inheritance + +HTMX 1.9.x and 2.x merge-inherit `hx-request`, `hx-vals`, and `hx-headers` automatically. +HTMX 4.x requires inheritance to be enabled explicitly. Use the corresponding Tag Helper attribute to emit +the HTMX 4 `inherited` modifier: + +| Razor attribute | Generated HTMX 4 attribute | +|-------------------------------|----------------------------| +| `hx-request-inherited="true"` | `hx-config:inherited` | +| `hx-vals-inherited="true"` | `hx-vals:inherited` | +| `hx-headers-inherited="true"` | `hx-headers:inherited` | + +For example, values declared on a parent element can be inherited by its children: + +```html +
+ ... +
+``` + +When `HtmxV4Config.MetaCharacter` is configured, that character is used instead of `:` in the generated attribute name. +Alternatively, set `HtmxV4Config.ImplicitInheritance` to `true` to enable inheritance globally. +For HTMX 1.9.x and 2.x, the Tag Helper attributes above do not change the generated attribute names. + ## Configuration Configure HTMX once during service registration. Only explicitly configured values are emitted, 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); } 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 = - { - ["категория"] = "Детские книги '