From 37b8804e7e7e5599d941ab875422f34b01dac8fe Mon Sep 17 00:00:00 2001 From: rameel Date: Wed, 19 Aug 2026 03:45:28 +0500 Subject: [PATCH 1/2] Add support for hx-vals attribute --- README.md | 24 ++++++++++ ...=> HtmxDictionaryJsonSerializerContext.cs} | 2 +- .../TagHelpers/HtmxHeaderTagHelper.cs | 2 +- .../TagHelpers/HtmxValsTagHelper.cs | 47 +++++++++++++++++++ .../HtmxValsTagHelperTests.cs | 42 +++++++++++++++++ 5 files changed, 115 insertions(+), 2 deletions(-) rename src/Ramstack.HtmxToolkit/TagHelpers/{HtmxHeaderJsonSerializerContext.cs => HtmxDictionaryJsonSerializerContext.cs} (74%) create mode 100644 src/Ramstack.HtmxToolkit/TagHelpers/HtmxValsTagHelper.cs create mode 100644 tests/Ramstack.HtmxToolkit.Tests/HtmxValsTagHelperTests.cs diff --git a/README.md b/README.md index 3ab9555..a5e86eb 100644 --- a/README.md +++ b/README.md @@ -582,6 +582,30 @@ you can assign them to the `hx-all-headers` attribute: The `HtmxHeaderTagHelper` will take care of all the remaining work regarding JSON serialization and escaping. +### HtmxValsTagHelper + +The `HtmxValsTagHelper` adds values that HTMX includes with a request. Use `hx-val-*` +attributes instead of writing JSON manually: + +```html + +``` + +The following HTML will be generated: + +```html + +``` + +You can also provide the values as a dictionary with the `hx-all-vals` attribute. + ### HtmxRequestTagHelper The `HtmxRequestTagHelper` configures the htmx request options supported by HTMX 1.x and 2.x. diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderJsonSerializerContext.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxDictionaryJsonSerializerContext.cs similarity index 74% rename from src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderJsonSerializerContext.cs rename to src/Ramstack.HtmxToolkit/TagHelpers/HtmxDictionaryJsonSerializerContext.cs index b780e5a..ae4ce45 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderJsonSerializerContext.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxDictionaryJsonSerializerContext.cs @@ -4,4 +4,4 @@ namespace Ramstack.HtmxToolkit.TagHelpers; [JsonSerializable(typeof(IDictionary))] [JsonSourceGenerationOptions(WriteIndented = false, GenerationMode = JsonSourceGenerationMode.Serialization)] -internal partial class HtmxHeaderJsonSerializerContext : JsonSerializerContext; +internal partial class HtmxDictionaryJsonSerializerContext : JsonSerializerContext; diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs index 635da26..fcdbc4e 100644 --- a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs @@ -36,7 +36,7 @@ public override Task ProcessAsync(TagHelperContext context, TagHelperOutput outp { if (Headers is { Count: > 0 }) { - var info = HtmxHeaderJsonSerializerContext.Default.IDictionaryStringString; + var info = HtmxDictionaryJsonSerializerContext.Default.IDictionaryStringString; var headers = new HtmlString(JsonSerializer.Serialize(Headers, info)); output.Attributes.SetAttribute( diff --git a/src/Ramstack.HtmxToolkit/TagHelpers/HtmxValsTagHelper.cs b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxValsTagHelper.cs new file mode 100644 index 0000000..2633317 --- /dev/null +++ b/src/Ramstack.HtmxToolkit/TagHelpers/HtmxValsTagHelper.cs @@ -0,0 +1,47 @@ +using System.Text.Json; + +using Microsoft.AspNetCore.Html; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Ramstack.HtmxToolkit.TagHelpers; + +/// +/// Represents a implementation that applies the hx-vals attribute to matching elements. +/// +/// +/// +/// hx-vals is inherited and can be placed on a parent element. +/// A child declaration of a value overrides a parent declaration. +/// +/// +[HtmlTargetElement(Attributes = ValuesDictionaryName)] +[HtmlTargetElement(Attributes = ValuesPrefix + "*")] +public sealed class HtmxValsTagHelper : TagHelper +{ + private const string ValuesPrefix = "hx-val-"; + private const string ValuesDictionaryName = "hx-all-vals"; + + /// + /// Gets or sets the hx-vals attribute values. + /// + [HtmlAttributeName(ValuesDictionaryName, DictionaryAttributePrefix = ValuesPrefix)] + public IDictionary Values + { + get => field ??= new Dictionary(); + set; + } + + /// + public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + if (Values is { Count: > 0 } values) + { + var json = JsonSerializer.Serialize(values, HtmxDictionaryJsonSerializerContext.Default.IDictionaryStringString); + var attribute = new TagHelperAttribute("hx-vals", new HtmlString(json), HtmlAttributeValueStyle.SingleQuotes); + + output.Attributes.SetAttribute(attribute); + } + + return Task.CompletedTask; + } +} diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxValsTagHelperTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxValsTagHelperTests.cs new file mode 100644 index 0000000..2dec58d --- /dev/null +++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxValsTagHelperTests.cs @@ -0,0 +1,42 @@ +namespace Ramstack.HtmxToolkit.Tests; + +[TestFixture] +public class HtmxValsTagHelperTests +{ + [Test] + public async Task ProcessAsync_SerializesValues() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = new HtmxValsTagHelper + { + Values = new Dictionary + { + ["category"] = "books", + ["sort"] = "title" + } + }; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + var attribute = output.Attributes["hx-vals"]; + + Assert.That(attribute, Is.Not.Null); + + var json = JsonHelper.ParseJson(attribute!.Value.ToString()!); + Assert.That(json["category"].GetString(), Is.EqualTo("books")); + Assert.That(json["sort"].GetString(), Is.EqualTo("title")); + } + + [Test] + public async Task ProcessAsync_OmitsEmptyDictionary() + { + var output = TestHelper.CreateTagHelperOutput(); + var helper = new HtmxValsTagHelper + { + Values = new Dictionary() + }; + + await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output); + + Assert.That(output.Attributes["hx-vals"], Is.Null); + } +} From d4dce5567e4f4055b395dc74f8014cd8a6bd813d Mon Sep 17 00:00:00 2001 From: rameel Date: Wed, 19 Aug 2026 03:58:13 +0500 Subject: [PATCH 2/2] Add demo examples --- .../Pages/Examples/HtmxVals.cshtml | 45 +++++++++++++++++++ .../Pages/Examples/HtmxVals.cshtml.cs | 22 +++++++++ .../Pages/Index.cshtml | 1 + .../Pages/Shared/_Layout.cshtml | 1 + 4 files changed, 69 insertions(+) create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxVals.cshtml create mode 100644 samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxVals.cshtml.cs diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxVals.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxVals.cshtml new file mode 100644 index 0000000..3eb8838 --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxVals.cshtml @@ -0,0 +1,45 @@ +@page +@model HtmxValsModel +@{ + ViewData["Title"] = "HTMX values"; +} + +
+
+

Example 11

+

Send values with HTMX

+

Add request parameters declaratively without assembling JSON by hand

+
+ +
+
+

hx-val-* and hx-all-vals

+

Both forms send values with the request and create an hx-vals attribute.

+
+ +
+ + +
+ +
+ Choose a value declaration. +
+
+
diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxVals.cshtml.cs b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxVals.cshtml.cs new file mode 100644 index 0000000..4a2a6bc --- /dev/null +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxVals.cshtml.cs @@ -0,0 +1,22 @@ +using System.Text.Encodings.Web; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Ramstack.HtmxToolkit.Demo.Pages.Examples; + +public class HtmxValsModel : PageModel +{ + public IActionResult OnGetShow(string? category, string? format) + { + return Content($""" +

Category: {Encode(category)}

+

Format: {Encode(format)}

+ """); + + static string Encode(string? text) => + text is { Length: > 0 } + ? HtmlEncoder.Default.Encode(text) + : "(not supplied)"; + } +} diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml index 54bff4d..691dc25 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml @@ -19,5 +19,6 @@

08. Boosted navigation

Detect links enhanced by HTMX.

09. On-demand content

Request and swap a small response.

10. Antiforgery

Post a protected form via HTMX.

+

11. HTMX values

Send additional request values declaratively.

diff --git a/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml b/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml index d98f437..e57bd1f 100644 --- a/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml +++ b/samples/Ramstack.HtmxToolkit.Demo/Pages/Shared/_Layout.cshtml @@ -38,6 +38,7 @@ 08. Boosted navigation 09. On-demand content 10. Antiforgery + 11. HTMX values