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
37 changes: 8 additions & 29 deletions src/Ramstack.HtmxToolkit/HtmxResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ namespace Ramstack.HtmxToolkit;
/// <summary>
/// Represents HTTP response to set htmx response headers.
/// </summary>
/// <remarks>
/// Like <see cref="HttpContext"/> and <see cref="HttpResponse"/> themselves, this type is not thread-safe.
/// Its members should not be called concurrently from multiple threads for the same request.
/// </remarks>
[DebuggerTypeProxy(typeof(HtmxResponseDebugView))]
public readonly struct HtmxResponse
{
Expand Down Expand Up @@ -211,7 +215,7 @@ public HtmxResponse TriggerEvent(string eventName, object detail, HtmxTriggerTim
return TriggerEventImpl(this, eventName, detail, timing);

static HtmxResponse TriggerEventImpl(HtmxResponse response, string eventName, object detail, HtmxTriggerTiming timing) =>
SetEvents(response, new Dictionary<string, object> { [eventName] = detail }, timing);
AddEvents(response, new Dictionary<string, object> { [eventName] = detail }, timing);
}

/// <summary>
Expand All @@ -224,7 +228,7 @@ static HtmxResponse TriggerEventImpl(HtmxResponse response, string eventName, ob
/// The current <see cref="HtmxResponse"/> instance.
/// </returns>
public HtmxResponse TriggerEvents(IReadOnlyDictionary<string, object> events, HtmxTriggerTiming timing = HtmxTriggerTiming.Receive) =>
SetEvents(this, events, timing);
AddEvents(this, events, timing);

/// <summary>
/// Sets the special HTTP status code <c>286</c> that is used to stop the polling.
Expand Down Expand Up @@ -256,34 +260,9 @@ private static HtmxResponse SetHeader(HtmxResponse response, string key, string
return response;
}

private static HtmxResponse SetEvents(HtmxResponse response, IReadOnlyDictionary<string, object> events, HtmxTriggerTiming timing)
private static HtmxResponse AddEvents(HtmxResponse response, IReadOnlyDictionary<string, object> events, HtmxTriggerTiming timing)
{
var key = timing switch
{
HtmxTriggerTiming.Receive => HtmxResponseHeaderNames.Trigger,
HtmxTriggerTiming.AfterSettle => HtmxResponseHeaderNames.TriggerAfterSettle,
_ => HtmxResponseHeaderNames.TriggerAfterSwap
};

if (response._response.Headers.TryGetValue(key, out var values))
{
var current = JsonSerializer.Deserialize<Dictionary<string, object>>(values.ToString())!;

if (events is Dictionary<string, object> dictionary)
{
foreach (var (k, v) in dictionary)
current.TryAdd(k, v);
}
else
{
foreach (var (k, v) in events)
current.TryAdd(k, v);
}

events = current;
}

response._response.Headers[key] = JsonSerializer.Serialize(events, JsonOptions.CamelCase);
PendingEvents.GetOrCreate(response._response).AddEvents(timing, events);
return response;
}

Expand Down
79 changes: 39 additions & 40 deletions src/Ramstack.HtmxToolkit/HtmxResponseHeaders.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;

using Microsoft.AspNetCore.Http;

using Ramstack.HtmxToolkit.Internal;
Expand All @@ -14,23 +12,23 @@ namespace Ramstack.HtmxToolkit;
[DebuggerTypeProxy(typeof(HtmxResponseHeadersDebugView))]
public sealed class HtmxResponseHeaders
{
private readonly IHeaderDictionary _headers;
private readonly HttpResponse _response;

/// <summary>
/// Initializes a new instance of the <see cref="HtmxResponseHeaders"/> structure.
/// </summary>
/// <param name="response">The HTTP response.</param>
internal HtmxResponseHeaders(HttpResponse response) =>
_headers = response.Headers;
_response = response;

/// <summary>
/// Gets or sets the <c>HX-Location</c> header to perform a client-side redirect without a full page reload.
/// </summary>
[MaybeNull]
public string Location
{
get => GetHeader(_headers, HtmxResponseHeaderNames.Location);
set => SetHeader(_headers, HtmxResponseHeaderNames.Location, value);
get => GetHeader(_response.Headers, HtmxResponseHeaderNames.Location);
set => SetHeader(_response.Headers, HtmxResponseHeaderNames.Location, value);
}

/// <summary>
Expand All @@ -39,8 +37,8 @@ public string Location
[MaybeNull]
public string PushUrl
{
get => GetHeader(_headers, HtmxResponseHeaderNames.PushUrl);
set => SetHeader(_headers, HtmxResponseHeaderNames.PushUrl, value);
get => GetHeader(_response.Headers, HtmxResponseHeaderNames.PushUrl);
set => SetHeader(_response.Headers, HtmxResponseHeaderNames.PushUrl, value);
}

/// <summary>
Expand All @@ -49,17 +47,17 @@ public string PushUrl
[MaybeNull]
public string Redirect
{
get => GetHeader(_headers, HtmxResponseHeaderNames.Redirect);
set => SetHeader(_headers, HtmxResponseHeaderNames.Redirect, value);
get => GetHeader(_response.Headers, HtmxResponseHeaderNames.Redirect);
set => SetHeader(_response.Headers, HtmxResponseHeaderNames.Redirect, value);
}

/// <summary>
/// Gets or sets the <c>HX-Refresh</c> header to perform a full page refresh.
/// </summary>
public bool Refresh
{
get => GetHeader(_headers, HtmxResponseHeaderNames.Refresh) == "true";
set => SetHeader(_headers, HtmxResponseHeaderNames.Refresh, value ? "true" : null);
get => GetHeader(_response.Headers, HtmxResponseHeaderNames.Refresh) == "true";
set => SetHeader(_response.Headers, HtmxResponseHeaderNames.Refresh, value ? "true" : null);
}

/// <summary>
Expand All @@ -69,8 +67,8 @@ public bool Refresh
[MaybeNull]
public string ReplaceUrl
{
get => GetHeader(_headers, HtmxResponseHeaderNames.ReplaceUrl);
set => SetHeader(_headers, HtmxResponseHeaderNames.ReplaceUrl, value);
get => GetHeader(_response.Headers, HtmxResponseHeaderNames.ReplaceUrl);
set => SetHeader(_response.Headers, HtmxResponseHeaderNames.ReplaceUrl, value);
}

/// <summary>
Expand All @@ -79,8 +77,8 @@ public string ReplaceUrl
[DisallowNull]
public HtmxSwap? Reswap
{
get => EnumHelper.ParseHtmxSwap(GetHeader(_headers, HtmxResponseHeaderNames.Reswap));
set => SetHeader(_headers, HtmxResponseHeaderNames.Reswap, value.GetValueOrDefault().GetSwapValue());
get => EnumHelper.ParseHtmxSwap(GetHeader(_response.Headers, HtmxResponseHeaderNames.Reswap));
set => SetHeader(_response.Headers, HtmxResponseHeaderNames.Reswap, value.GetValueOrDefault().GetSwapValue());
}

/// <summary>
Expand All @@ -89,8 +87,8 @@ public HtmxSwap? Reswap
[MaybeNull]
public string ReswapExpression
{
get => GetHeader(_headers, HtmxResponseHeaderNames.Reswap);
set => SetHeader(_headers, HtmxResponseHeaderNames.Reswap, value);
get => GetHeader(_response.Headers, HtmxResponseHeaderNames.Reswap);
set => SetHeader(_response.Headers, HtmxResponseHeaderNames.Reswap, value);
}

/// <summary>
Expand All @@ -100,8 +98,8 @@ public string ReswapExpression
[MaybeNull]
public string Retarget
{
get => GetHeader(_headers, HtmxResponseHeaderNames.Retarget);
set => SetHeader(_headers, HtmxResponseHeaderNames.Retarget, value);
get => GetHeader(_response.Headers, HtmxResponseHeaderNames.Retarget);
set => SetHeader(_response.Headers, HtmxResponseHeaderNames.Retarget, value);
}

/// <summary>
Expand All @@ -111,41 +109,53 @@ public string Retarget
[MaybeNull]
public string Reselect
{
get => GetHeader(_headers, HtmxResponseHeaderNames.Reselect);
set => SetHeader(_headers, HtmxResponseHeaderNames.Reselect, value);
get => GetHeader(_response.Headers, HtmxResponseHeaderNames.Reselect);
set => SetHeader(_response.Headers, HtmxResponseHeaderNames.Reselect, value);
}

/// <summary>
/// Gets or sets the <c>HX-Trigger</c> header, which specifies a dictionary of client-side events
/// to trigger after the server response is processed.
/// </summary>
/// <remarks>
/// Event values are accumulated for the current response and serialized into the header immediately
/// before the response starts.
/// </remarks>
[MaybeNull]
public IReadOnlyDictionary<string, object> Trigger
{
get => GetEvents(_headers, HtmxResponseHeaderNames.Trigger);
set => SetEvents(_headers, HtmxResponseHeaderNames.Trigger, value);
get => PendingEvents.TryGet(_response)?.GetEvents(HtmxTriggerTiming.Receive);
set => PendingEvents.GetOrCreate(_response).SetEvents(HtmxTriggerTiming.Receive, value);
}

/// <summary>
/// Gets or sets the <c>HX-Trigger-After-Settle</c> header, which specifies a dictionary of client-side events
/// to trigger after the htmx request has settled.
/// </summary>
/// <remarks>
/// Event values are accumulated for the current response and serialized into the header immediately
/// before the response starts.
/// </remarks>
[MaybeNull]
public IReadOnlyDictionary<string, object> TriggerAfterSettle
{
get => GetEvents(_headers, HtmxResponseHeaderNames.TriggerAfterSettle);
set => SetEvents(_headers, HtmxResponseHeaderNames.TriggerAfterSettle, value);
get => PendingEvents.TryGet(_response)?.GetEvents(HtmxTriggerTiming.AfterSettle);
set => PendingEvents.GetOrCreate(_response).SetEvents(HtmxTriggerTiming.AfterSettle, value);
}

/// <summary>
/// Gets or sets the <c>HX-Trigger-After-Swap</c> header, which specifies a dictionary of client-side events
/// to trigger after the response content has been swapped into the DOM.
/// </summary>
/// <remarks>
/// Event values are accumulated for the current response and serialized into the header immediately
/// before the response starts.
/// </remarks>
[MaybeNull]
public IReadOnlyDictionary<string, object> TriggerAfterSwap
{
get => GetEvents(_headers, HtmxResponseHeaderNames.TriggerAfterSwap);
set => SetEvents(_headers, HtmxResponseHeaderNames.TriggerAfterSwap, value);
get => PendingEvents.TryGet(_response)?.GetEvents(HtmxTriggerTiming.AfterSwap);
set => PendingEvents.GetOrCreate(_response).SetEvents(HtmxTriggerTiming.AfterSwap, value);
}

private static string? GetHeader(IHeaderDictionary headers, string key)
Expand All @@ -160,23 +170,12 @@ private static void SetHeader(IHeaderDictionary headers, string key, string? val
headers[key] = value;
}

private static Dictionary<string, object>? GetEvents(IHeaderDictionary headers, string key)
{
if (headers.TryGetValue(key, out var values))
return JsonSerializer.Deserialize<Dictionary<string, object>>(values.ToString());

return null;
}

private static void SetEvents(IHeaderDictionary headers, string key, IReadOnlyDictionary<string, object> events) =>
headers[key] = JsonSerializer.Serialize(events, JsonOptions.CamelCase);

#region Inner type: HtmxResponseHeadersDebugView

private sealed class HtmxResponseHeadersDebugView(HtmxResponseHeaders headers)
{
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
public KeyValuePair<string, string>[] Items => DebugHelpers.GetHeaders(headers._headers);
public KeyValuePair<string, string>[] Items => DebugHelpers.GetHeaders(headers._response.Headers);
}

#endregion
Expand Down
Loading
Loading