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 =
- {
- ["категория"] = "Детские книги '