From 75ab106fc92f7297655a1ff617388dacb92eb259 Mon Sep 17 00:00:00 2001 From: Ricky Tobing Date: Wed, 11 Oct 2023 12:38:54 -0400 Subject: [PATCH 1/4] HtmxHeaderTagHelper initial checkin + added sample --- src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs | 54 +++++++++++++++++++ test/Sample/Pages/Headers.cshtml | 58 +++++++++++++++++++++ test/Sample/Pages/Headers.cshtml.cs | 20 +++++++ test/Sample/Pages/Shared/_Layout.cshtml | 3 ++ 4 files changed, 135 insertions(+) create mode 100644 src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs create mode 100644 test/Sample/Pages/Headers.cshtml create mode 100644 test/Sample/Pages/Headers.cshtml.cs diff --git a/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs b/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs new file mode 100644 index 0000000..81f7528 --- /dev/null +++ b/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs @@ -0,0 +1,54 @@ +using JetBrains.Annotations; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Collections.Generic; +using System; +using System.Text.Json; +using System.Text.Json.Nodes; + +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-post]")] + [HtmlTargetElement("*", Attributes = "[hx-put]")] + [HtmlTargetElement("*", Attributes = "[hx-delete]")] + [HtmlTargetElement("*", Attributes = "[hx-patch]")] + public class HxHeaders : TagHelper + { + /// + /// JSON formatted string, a Dictionary or any object. + /// + [HtmlAttributeName("hx-headers")] + public object Headers { get; set; } = default!; + + /// + /// Dictionary of hx-headers's html attributes + /// + [HtmlAttributeName("hx-headers", DictionaryAttributePrefix = "hx-headers-")] + public IDictionary HeaderAttributes { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + var jsonObject = Headers switch + { + string headerJson => JsonSerializer.Deserialize(headerJson), + null => null, + _ => JsonSerializer.Deserialize(JsonSerializer.Serialize(Headers)) + } ?? new JsonObject(); + + // merge + foreach (var jObject in jsonObject) + { + HeaderAttributes[jObject.Key] = jObject.Value?.ToString(); + } + + // serialize + var json = JsonSerializer.Serialize(HeaderAttributes); + + output.Attributes.Add("hx-headers", json); + } + } +} diff --git a/test/Sample/Pages/Headers.cshtml b/test/Sample/Pages/Headers.cshtml new file mode 100644 index 0000000..6058476 --- /dev/null +++ b/test/Sample/Pages/Headers.cshtml @@ -0,0 +1,58 @@ +@page +@using Microsoft.AspNetCore.Antiforgery; +@inject IAntiforgery antiforgery; +@model HxRequestsModel + +
+ +
+ +
+ +
+ +
+ +
+ +
+ 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 @@ + From bfd76948787f99ccb9fcf1f838b06c2c22357809 Mon Sep 17 00:00:00 2001 From: Ricky Tobing Date: Wed, 11 Oct 2023 20:25:15 -0400 Subject: [PATCH 2/4] Ignore when there's an existing hx-headers value --- src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs | 22 +++++---------------- test/Sample/Pages/Headers.cshtml | 19 ++++-------------- 2 files changed, 9 insertions(+), 32 deletions(-) diff --git a/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs b/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs index 81f7528..2f38362 100644 --- a/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs +++ b/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs @@ -4,6 +4,7 @@ using System; using System.Text.Json; using System.Text.Json.Nodes; +using System.Linq; namespace Htmx.TagHelpers { @@ -18,31 +19,18 @@ namespace Htmx.TagHelpers [HtmlTargetElement("*", Attributes = "[hx-patch]")] public class HxHeaders : TagHelper { - /// - /// JSON formatted string, a Dictionary or any object. - /// - [HtmlAttributeName("hx-headers")] - public object Headers { get; set; } = default!; - /// /// Dictionary of hx-headers's html attributes /// - [HtmlAttributeName("hx-headers", DictionaryAttributePrefix = "hx-headers-")] + [HtmlAttributeName(DictionaryAttributePrefix = "hx-headers-")] public IDictionary HeaderAttributes { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public override void Process(TagHelperContext context, TagHelperOutput output) { - var jsonObject = Headers switch - { - string headerJson => JsonSerializer.Deserialize(headerJson), - null => null, - _ => JsonSerializer.Deserialize(JsonSerializer.Serialize(Headers)) - } ?? new JsonObject(); - - // merge - foreach (var jObject in jsonObject) + var existingHeaders = output.Attributes["hx-headers"]?.Value; + if (existingHeaders != null || !HeaderAttributes.Any()) { - HeaderAttributes[jObject.Key] = jObject.Value?.ToString(); + return; } // serialize diff --git a/test/Sample/Pages/Headers.cshtml b/test/Sample/Pages/Headers.cshtml index 6058476..121c864 100644 --- a/test/Sample/Pages/Headers.cshtml +++ b/test/Sample/Pages/Headers.cshtml @@ -3,17 +3,6 @@ @inject IAntiforgery antiforgery; @model HxRequestsModel -
- -
-
From 615007366ce050444b7483f9d8b653e468686d37 Mon Sep 17 00:00:00 2001 From: Ricky Tobing Date: Wed, 11 Oct 2023 20:54:50 -0400 Subject: [PATCH 3/4] Added HtmxHeadersTagHelperTests + Code cleanup --- src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs | 11 ++- test/Htmx.Tests/Htmx.Tests.csproj | 1 + .../TagHelpers/HtmxHeadersTagHelperTests.cs | 92 +++++++++++++++++++ test/Sample/Pages/Headers.cshtml | 11 +-- 4 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 test/Htmx.Tests/TagHelpers/HtmxHeadersTagHelperTests.cs diff --git a/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs b/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs index 2f38362..d75e7e6 100644 --- a/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs +++ b/src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System; using System.Text.Json; -using System.Text.Json.Nodes; using System.Linq; namespace Htmx.TagHelpers @@ -13,22 +12,24 @@ namespace Htmx.TagHelpers /// 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 HxHeaders : TagHelper + public class HtmxHeadersTagHelper : TagHelper { /// - /// Dictionary of hx-headers's html attributes + /// 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 existingHeaders = output.Attributes["hx-headers"]?.Value; - if (existingHeaders != null || !HeaderAttributes.Any()) + var existingHxHeaders = output.Attributes["hx-headers"]?.Value; + if (existingHxHeaders != null || !HeaderAttributes.Any()) { return; } 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 index 121c864..b2fd899 100644 --- a/test/Sample/Pages/Headers.cshtml +++ b/test/Sample/Pages/Headers.cshtml @@ -7,9 +7,8 @@ +
+ Testing: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields +
+ +
+
requestConfig.headers