diff --git a/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs b/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs new file mode 100644 index 0000000..d75e7e6 --- /dev/null +++ b/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs @@ -0,0 +1,43 @@ +using JetBrains.Annotations; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Collections.Generic; +using System; +using System.Text.Json; +using System.Linq; + +namespace Htmx.TagHelpers +{ + /// + /// Targets any element that has hx-get, hx-post, hx-put, hx-patch, and hx-delete. + /// https://htmx.org/attributes/hx-headers/ + /// + [PublicAPI] + [HtmlTargetElement("*", Attributes = "[hx-get]")] + [HtmlTargetElement("*", Attributes = "[hx-post]")] + [HtmlTargetElement("*", Attributes = "[hx-put]")] + [HtmlTargetElement("*", Attributes = "[hx-delete]")] + [HtmlTargetElement("*", Attributes = "[hx-patch]")] + public class HtmxHeadersTagHelper : TagHelper + { + /// + /// Dictionary of hx-headers + /// + [HtmlAttributeName(DictionaryAttributePrefix = "hx-headers-")] + public IDictionary HeaderAttributes { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + var existingHxHeaders = output.Attributes["hx-headers"]?.Value; + if (existingHxHeaders != null || !HeaderAttributes.Any()) + { + return; + } + + // serialize + var json = JsonSerializer.Serialize(HeaderAttributes); + + output.Attributes.Add("hx-headers", json); + } + } +} diff --git a/test/Htmx.Tests/Htmx.Tests.csproj b/test/Htmx.Tests/Htmx.Tests.csproj index 06c74ea..d585b7c 100644 --- a/test/Htmx.Tests/Htmx.Tests.csproj +++ b/test/Htmx.Tests/Htmx.Tests.csproj @@ -25,6 +25,7 @@ + diff --git a/test/Htmx.Tests/TagHelpers/HtmxHeadersTagHelperTests.cs b/test/Htmx.Tests/TagHelpers/HtmxHeadersTagHelperTests.cs new file mode 100644 index 0000000..da41826 --- /dev/null +++ b/test/Htmx.Tests/TagHelpers/HtmxHeadersTagHelperTests.cs @@ -0,0 +1,92 @@ +using Htmx.TagHelpers; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Xunit; + +namespace Htmx.Tests.TagHelpers +{ + public class HtmxHeadersTagHelperTests + { + private readonly HtmxHeadersTagHelper _tagHelper; + private readonly TagHelperContext _context; + private readonly TagHelperOutput _output; + + public HtmxHeadersTagHelperTests() + { + _tagHelper = new HtmxHeadersTagHelper(); + + _context = new TagHelperContext( + new TagHelperAttributeList(), + new Dictionary(), + "test_unique_id"); + + _output = new TagHelperOutput( + "div", + new TagHelperAttributeList(), + (useCachedResult, encoder) => + { + var tagHelperContent = new DefaultTagHelperContent(); + var tagBuilder = new TagBuilder("div"); + tagBuilder.Attributes.Add("hx-post", "url"); + tagHelperContent.SetHtmlContent(tagBuilder); + return Task.FromResult(tagHelperContent); + }); + } + + [Fact] + public void Is_HeaderAttributes_NotNull() + { + Assert.NotNull(_tagHelper.HeaderAttributes); + Assert.Empty(_tagHelper.HeaderAttributes); + } + + [Fact] + public void Process_AddsHeadersAttribute_WhenNoExistingHeadersAndHeaderAttributesPresent() + { + // Arrange + _tagHelper.HeaderAttributes.Add("Key1", "Value1"); + _tagHelper.HeaderAttributes.Add("Key2", "Value2"); + + // Act + _tagHelper.Process(_context, _output); + + // Assert + Assert.True(_output.Attributes.TryGetAttribute("hx-headers", out var headersAttribute)); + Assert.NotNull(headersAttribute); + + var json = JsonSerializer.Deserialize(headersAttribute.Value.ToString()!)!; + Assert.Equal("Value1", json["Key1"]!.GetValue()); + Assert.Equal("Value2", json["Key2"]!.GetValue()); + } + + [Fact] + public void Process_DoesNotAddHeadersAttribute_WhenExistingHeadersPresent() + { + // Arrange + var existingHeaders = new TagHelperAttribute("hx-headers", "{\"ExistingKey1\":\"ExistingValue1\"}"); + _output.Attributes.Add(existingHeaders); + + // Act + _tagHelper.Process(_context, _output); + + // Assert + var json = JsonSerializer.Deserialize(_output.Attributes["hx-headers"].Value.ToString()!)!; + Assert.Equal("ExistingValue1", json["ExistingKey1"]!.GetValue()); + } + + [Fact] + public void Process_DoesNotAddHeadersAttribute_WhenNoHeaderAttributes() + { + // Act + _tagHelper.Process(_context, _output); + + // Assert + Assert.False(_output.Attributes.TryGetAttribute("hx-headers", out _)); + } + } + +} diff --git a/test/Sample/Pages/Headers.cshtml b/test/Sample/Pages/Headers.cshtml new file mode 100644 index 0000000..046cc76 --- /dev/null +++ b/test/Sample/Pages/Headers.cshtml @@ -0,0 +1,63 @@ +@page +@using Microsoft.AspNetCore.Antiforgery; +@inject IAntiforgery antiforgery; +@model HxRequestsModel + +
+ +
+ +
+ +
+ +
+ Testing: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields +
+ +
+ +
+ requestConfig.headers + +
+ +@section Scripts { + +} + diff --git a/test/Sample/Pages/Headers.cshtml.cs b/test/Sample/Pages/Headers.cshtml.cs new file mode 100644 index 0000000..a2d3048 --- /dev/null +++ b/test/Sample/Pages/Headers.cshtml.cs @@ -0,0 +1,20 @@ +using Htmx; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using System.Text.Json; + +namespace Sample.Pages +{ + [ValidateAntiForgeryToken] + public class HxRequestsModel : PageModel + { + public IActionResult OnPost() + { + // list of headers + var headers = Request.Headers.ToList(); + var html = "
" + JsonSerializer.Serialize(headers, new JsonSerializerOptions { WriteIndented = true }) + "
"; + + return Content(html, "text/html"); + } + } +} diff --git a/test/Sample/Pages/Shared/_Layout.cshtml b/test/Sample/Pages/Shared/_Layout.cshtml index f1fa0ef..a0d157e 100644 --- a/test/Sample/Pages/Shared/_Layout.cshtml +++ b/test/Sample/Pages/Shared/_Layout.cshtml @@ -37,6 +37,9 @@ +