Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding hx-headers-* server-side processing to hx-headers #36

Merged
merged 4 commits into from
Oct 16, 2023
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
43 changes: 43 additions & 0 deletions src/Htmx.TagHelpers/HtmxHeadersTagHelper.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Targets any element that has hx-get, hx-post, hx-put, hx-patch, and hx-delete.
/// https://htmx.org/attributes/hx-headers/
/// </summary>
[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
{
/// <summary>
/// Dictionary of hx-headers
/// </summary>
[HtmlAttributeName(DictionaryAttributePrefix = "hx-headers-")]
public IDictionary<string, string?> HeaderAttributes { get; set; } = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);

/// <inheritdoc />
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);
}
}
}
1 change: 1 addition & 0 deletions test/Htmx.Tests/Htmx.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Htmx.TagHelpers\Htmx.TagHelpers.csproj" />
<ProjectReference Include="..\..\src\Htmx\Htmx.csproj" />
</ItemGroup>

Expand Down
92 changes: 92 additions & 0 deletions test/Htmx.Tests/TagHelpers/HtmxHeadersTagHelperTests.cs
Original file line number Diff line number Diff line change
@@ -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<object, object>(),
"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>(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<JsonObject>(headersAttribute.Value.ToString()!)!;
Assert.Equal("Value1", json["Key1"]!.GetValue<string>());
Assert.Equal("Value2", json["Key2"]!.GetValue<string>());
}

[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<JsonObject>(_output.Attributes["hx-headers"].Value.ToString()!)!;
Assert.Equal("ExistingValue1", json["ExistingKey1"]!.GetValue<string>());
}

[Fact]
public void Process_DoesNotAddHeadersAttribute_WhenNoHeaderAttributes()
{
// Act
_tagHelper.Process(_context, _output);

// Assert
Assert.False(_output.Attributes.TryGetAttribute("hx-headers", out _));
}
}

}
63 changes: 63 additions & 0 deletions test/Sample/Pages/Headers.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@page
@using Microsoft.AspNetCore.Antiforgery;
@inject IAntiforgery antiforgery;
@model HxRequestsModel

<div>
<button type="button"
hx-post
hx-page="./Headers"
hx-headers-New-Key1="NewValue1"
hx-headers-New-Key2="NewValue2"
hx-swap="innerHTML"
hx-target="#post-result">
Click me (using hx-headers-key="value")
</button>
</div>

<div>
<button type="button"
hx-post
hx-page="./Headers"
hx-headers='{ "Existing-Key1": "ExistingValue1", "Existing-Key2": "ExistingValue1" }'
hx-headers-New-Key1="NewValue1"
hx-headers-New-Key2="NewValue2"
hx-swap="innerHTML"
hx-target="#post-result">
Click me (with existing hx-headers)
</button>
</div>

<div>
Testing: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
<br/>
<button type="button"
hx-post
hx-page="./Headers"
hx-headers-X-Forwarded-Host="en.wikipedia.org:8080"
hx-headers-X-Front-End-Https="on"
hx-headers-X-Forwarded-For="client1, proxy1, proxy2"
hx-headers-Upgrade-Insecure-Requests="1"
hx-headers-X-LongCustom-Header="Stuff"
hx-swap="innerHTML"
hx-target="#post-result">
Click me (List_of_HTTP_header_fields)
</button>
</div>

<div class="mt-2">
<code>requestConfig.headers</code>
<textarea class="w-100" id="post-result" rows="10"></textarea>
</div>

@section Scripts {
<script type="text/javascript">
window.addEventListener("htmx:afterRequest", evt => {
let headers = evt.detail.requestConfig.headers;
let json = JSON.stringify(headers, null, 2);

document.querySelector('#post-result').innerHTML = json;
});
</script>
}

20 changes: 20 additions & 0 deletions test/Sample/Pages/Headers.cshtml.cs
Original file line number Diff line number Diff line change
@@ -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 = "<pre>" + JsonSerializer.Serialize(headers, new JsonSerializerOptions { WriteIndented = true }) + "</pre>";

return Content(html, "text/html");
}
}
}
3 changes: 3 additions & 0 deletions test/Sample/Pages/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Response">Response.Htmx()</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Headers"><code>hx-headers</code></a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Privacy">Privacy</a>
</li>
Expand Down
Loading