Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<button hx-get="/books"
hx-val-category="science"
hx-val-format="summary">
Browse books
</button>
```

The following HTML will be generated:

```html
<button hx-get="/books"
hx-vals='{"category":"science","format":"summary"}'>
Browse books
</button>
```

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.
Expand Down
45 changes: 45 additions & 0 deletions samples/Ramstack.HtmxToolkit.Demo/Pages/Examples/HtmxVals.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
@page
@model HtmxValsModel
@{
ViewData["Title"] = "HTMX values";
}

<article class="example-page">
<header class="page-heading">
<p class="eyebrow">Example 11</p>
<h1>Send values with HTMX</h1>
<p>Add request parameters declaratively without assembling JSON by hand</p>
</header>

<section class="demo-card">
<div class="demo-card__header">
<h2><code>hx-val-*</code> and <code>hx-all-vals</code></h2>
<p>Both forms send values with the request and create an <code>hx-vals</code> attribute.</p>
</div>

<div class="demo-actions">
<button
hx-get
hx-page="/Examples/HtmxVals"
hx-page-handler="Show"
hx-val-category="science"
hx-val-format="summary"
hx-target="#values-result">
Send individual values
</button>
<button
class="button button-secondary"
hx-get
hx-page="/Examples/HtmxVals"
hx-page-handler="Show"
hx-all-vals="@(new Dictionary<string, string> { { "category", "history" }, { "format", "full" } })"
hx-target="#values-result">
Send values dictionary
</button>
</div>

<div id="values-result" class="result">
Choose a value declaration.
</div>
</section>
</article>
Original file line number Diff line number Diff line change
@@ -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($"""
<p><b>Category:</b> {Encode(category)}</p>
<p><b>Format:</b> {Encode(format)}</b></p>
""");

static string Encode(string? text) =>
text is { Length: > 0 }
? HtmlEncoder.Default.Encode(text)
: "(not supplied)";
}
}
1 change: 1 addition & 0 deletions samples/Ramstack.HtmxToolkit.Demo/Pages/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
<a class="overview-card" asp-page="/Examples/Boosted"><h2>08. Boosted navigation</h2><p>Detect links enhanced by HTMX.</p></a>
<a class="overview-card" asp-page="/Examples/Random"><h2>09. On-demand content</h2><p>Request and swap a small response.</p></a>
<a class="overview-card" asp-page="/Examples/Antiforgery"><h2>10. Antiforgery</h2><p>Post a protected form via HTMX.</p></a>
<a class="overview-card" asp-page="/Examples/HtmxVals"><h2>11. HTMX values</h2><p>Send additional request values declaratively.</p></a>
</div>
</article>
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<a class="nav-link" asp-page="/Examples/Boosted">08. Boosted navigation</a>
<a class="nav-link" asp-page="/Examples/Random">09. On-demand content</a>
<a class="nav-link" asp-page="/Examples/Antiforgery">10. Antiforgery</a>
<a class="nav-link" asp-page="/Examples/HtmxVals">11. HTMX values</a>
</nav>
</aside>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ namespace Ramstack.HtmxToolkit.TagHelpers;

[JsonSerializable(typeof(IDictionary<string, string>))]
[JsonSourceGenerationOptions(WriteIndented = false, GenerationMode = JsonSourceGenerationMode.Serialization)]
internal partial class HtmxHeaderJsonSerializerContext : JsonSerializerContext;
internal partial class HtmxDictionaryJsonSerializerContext : JsonSerializerContext;
2 changes: 1 addition & 1 deletion src/Ramstack.HtmxToolkit/TagHelpers/HtmxHeaderTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
47 changes: 47 additions & 0 deletions src/Ramstack.HtmxToolkit/TagHelpers/HtmxValsTagHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.Text.Json;

using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace Ramstack.HtmxToolkit.TagHelpers;

/// <summary>
/// Represents a <see cref="TagHelper"/> implementation that applies the <c>hx-vals</c> attribute to matching elements.
/// </summary>
/// <remarks>
/// <list type="bullet">
/// <item><c>hx-vals</c> is inherited and can be placed on a parent element.</item>
/// <item>A child declaration of a value overrides a parent declaration.</item>
/// </list>
/// </remarks>
[HtmlTargetElement(Attributes = ValuesDictionaryName)]
[HtmlTargetElement(Attributes = ValuesPrefix + "*")]
public sealed class HtmxValsTagHelper : TagHelper
{
private const string ValuesPrefix = "hx-val-";
private const string ValuesDictionaryName = "hx-all-vals";

/// <summary>
/// Gets or sets the <c>hx-vals</c> attribute values.
/// </summary>
[HtmlAttributeName(ValuesDictionaryName, DictionaryAttributePrefix = ValuesPrefix)]
public IDictionary<string, string> Values
{
get => field ??= new Dictionary<string, string>();
set;
}

/// <inheritdoc />
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;
}
}
42 changes: 42 additions & 0 deletions tests/Ramstack.HtmxToolkit.Tests/HtmxValsTagHelperTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, string>
{
["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<string, string>()
};

await helper.ProcessAsync(TestHelper.CreateTagHelperContext(), output);

Assert.That(output.Attributes["hx-vals"], Is.Null);
}
}
Loading