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
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@

<section class="demo-card">
<div class="demo-card__header">
<h2><code>hx-request-timeout</code></h2>
<p>The toolkit emits an HTMX 4 <code>hx-config</code> timeout for this report preview.</p>
<h2><code>hx-request-*</code></h2>
<p>The toolkit emits an HTMX 4 <code>hx-config</code> with a timeout, cache mode, and redirect mode for this report preview.</p>
</div>

<div class="demo-actions">
<button hx-get
hx-page="/Examples/RequestOptions"
hx-page-handler="Preview"
hx-request-timeout="2000"
hx-request-cache="@HtmxRequestCache.NoCache"
hx-request-redirect="@HtmxRequestRedirect.Error"
hx-target="#preview-result">
Generate report preview
</button>
Expand Down
10 changes: 0 additions & 10 deletions src/Ramstack.HtmxToolkit/Configuration/HtmxV1Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,16 +182,6 @@ public bool? UseTemplateFragments
set => SetField(ref field, value);
}

/// <summary>
/// Gets or sets the WebSocket reconnection delay strategy.
/// The HTMX default is <c>full-jitter</c>.
/// </summary>
public string? WsReconnectDelay
{
get;
set => SetField(ref field, value);
}

/// <summary>
/// Gets or sets the type of binary data received over WebSocket connections.
/// The HTMX default is <see cref="HtmxBinaryType.Blob" />.
Expand Down
11 changes: 0 additions & 11 deletions src/Ramstack.HtmxToolkit/Configuration/HtmxV2Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,17 +182,6 @@ public string[]? AttributesToSettle
set => SetField(ref field, value);
}

/// <summary>
/// Gets or sets the WebSocket reconnection delay strategy.
/// The HTMX default is <c>full-jitter</c>.
/// </summary>
/// <remarks>Requires the HTMX WebSocket extension.</remarks>
public string? WsReconnectDelay
{
get;
set => SetField(ref field, value);
}

/// <summary>
/// Gets or sets the type of binary data received over WebSocket connections.
/// The HTMX default is <see cref="HtmxBinaryType.Blob" />.
Expand Down
8 changes: 6 additions & 2 deletions src/Ramstack.HtmxToolkit/Configuration/HtmxV4Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,13 @@ public string? InlineScriptNonce
}

/// <summary>
/// Gets or sets a comma-separated list of extensions that HTMX is allowed to load.
/// The HTMX default is an empty string.
/// Gets or sets a comma-separated list of extensions that HTMX is allowed to register.
/// The HTMX default is an empty string, which allows all extensions.
/// </summary>
/// <remarks>
/// HTMX compares extension names exactly. Whitespace is allowed only after a comma;
/// any other whitespace becomes part of a name, so <c>"sse ,ws"</c> does not allow <c>sse</c>.
/// </remarks>
public string? Extensions
{
get;
Expand Down
24 changes: 22 additions & 2 deletions src/Ramstack.HtmxToolkit/Configuration/ResponseHandlingConfig.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
using System.Text.Json.Serialization;

using Ramstack.HtmxToolkit.Internal;

namespace Ramstack.HtmxToolkit.Configuration;

/// <summary>
Expand Down Expand Up @@ -37,7 +41,23 @@ public sealed class ResponseHandlingConfig
public string? Target { get; set; }

/// <summary>
/// Gets or sets an alternative swap mechanism for the response.
/// Gets or sets an alternative swap style for the response.
/// </summary>
/// <remarks>
/// Reads the style from <see cref="SwapOverrideExpression" />, or returns <see langword="null" />
/// if the style is not recognized. Assigning a style replaces the complete expression,
/// including any modifiers. Assigning <see langword="null" /> clears the override.
/// </remarks>
[JsonIgnore]
public HtmxSwap? SwapOverride
{
get => EnumHelper.ParseHtmxSwap(SwapOverrideExpression);
set => SwapOverrideExpression = value.GetSwapValue();
}

/// <summary>
/// Gets or sets the complete swap override expression, including any swap modifiers.
/// </summary>
public string? SwapOverride { get; set; }
[JsonPropertyName("swapOverride")]
public string? SwapOverrideExpression { get; set; }
}
21 changes: 20 additions & 1 deletion src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Ramstack.HtmxToolkit.Internal;

namespace Ramstack.HtmxToolkit;

/// <summary>
Expand All @@ -20,8 +22,25 @@ public sealed class HtmxLocationOptions
/// <summary>
/// Gets or sets how the response will be swapped relative to the target element.
/// </summary>
/// <remarks>
/// <para>Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x.</para>
/// <para>
/// Reads the style from <see cref="SwapExpression" />, or returns <see langword="null" />
/// if the style is not recognized. Assigning a style replaces the complete expression,
/// including any modifiers. Assigning <see langword="null" /> clears the swap.
/// </para>
/// </remarks>
public HtmxSwap? Swap
{
get => EnumHelper.ParseHtmxSwap(SwapExpression);
set => SwapExpression = value.GetSwapValue();
}

/// <summary>
/// Gets or sets the complete swap expression, including any swap modifiers.
/// </summary>
/// <remarks>Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x.</remarks>
public HtmxSwap? Swap { get; set; }
public string? SwapExpression { get; set; }

/// <summary>
/// Gets or sets the form field values to submit with the request.
Expand Down
42 changes: 42 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxRequestCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Ramstack.HtmxToolkit;

/// <summary>
/// Specifies the Fetch cache mode for an HTMX request.
/// </summary>
/// <remarks>Supported only in HTMX 4.x.</remarks>
public enum HtmxRequestCache
{
/// <summary>
/// Uses the browser's normal HTTP cache behavior.
/// </summary>
Default,

/// <summary>
/// Bypasses the cache and does not store the response in it.
/// </summary>
NoStore,

/// <summary>
/// Bypasses the cache lookup and updates the cache with the response.
/// </summary>
Reload,

/// <summary>
/// Revalidates a cached response before using it.
/// </summary>
NoCache,

/// <summary>
/// Uses a cached response even if stale, or fetches and caches a response on a cache miss.
/// </summary>
ForceCache,

/// <summary>
/// Uses a cached response even if stale, and fails on a cache miss.
/// </summary>
/// <remarks>
/// Requires the same-origin Fetch mode, which is the HTMX default.
/// The request fails if <see cref="Configuration.HtmxV4Config.Mode" /> specifies another mode.
/// </remarks>
OnlyIfCached
}
31 changes: 31 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxRequestRedirect.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Ramstack.HtmxToolkit;

/// <summary>
/// Specifies the Fetch redirect mode for an HTMX request.
/// </summary>
/// <remarks>Supported only in HTMX 4.x.</remarks>
public enum HtmxRequestRedirect
{
/// <summary>
/// Follows redirects automatically.
/// </summary>
Follow,

/// <summary>
/// Fails the request when a redirect is encountered.
/// </summary>
/// <remarks>
/// HTMX raises the <c>htmx:error</c> event and does not swap the response.
/// </remarks>
Error,

/// <summary>
/// Returns an opaque redirect response instead of following the redirect.
/// </summary>
/// <remarks>
/// The response has status 0, and its headers and body are not accessible.
/// HTMX cannot read <c>HX-*</c> response headers, does not treat the response as an error,
/// and swaps the empty body into the target.
/// </remarks>
Manual
}
8 changes: 4 additions & 4 deletions src/Ramstack.HtmxToolkit/HtmxResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,10 @@ public HtmxResponse Reselect(string value) =>
SetHeader(this, HtmxResponseHeaderNames.Reselect, value);

/// <summary>
/// Adds a client-side event to the response header selected by <paramref name="trigger" />.
/// Adds a client-side event to the response header selected by <paramref name="timing" />.
/// </summary>
/// <param name="eventName">The event name to trigger.</param>
/// <param name="trigger">The event timing. Defaults to <see cref="HtmxTriggerTiming.Receive" />.</param>
/// <param name="timing">The event timing. Defaults to <see cref="HtmxTriggerTiming.Receive" />.</param>
/// <returns>
/// The current <see cref="HtmxResponse" /> instance.
/// </returns>
Expand All @@ -209,8 +209,8 @@ public HtmxResponse Reselect(string value) =>
/// <c>HX-Trigger</c> and runs when the request completes (after the swap whenever one is performed).
/// See <see href="https://github.com/bigskysoftware/htmx/pull/3900">PR #3900</see>.
/// </remarks>
public HtmxResponse TriggerEvent(string eventName, HtmxTriggerTiming trigger = HtmxTriggerTiming.Receive) =>
AddToPendingEvent(this, eventName, "{}", trigger);
public HtmxResponse TriggerEvent(string eventName, HtmxTriggerTiming timing = HtmxTriggerTiming.Receive) =>
AddToPendingEvent(this, eventName, "{}", timing);

/// <summary>
/// Adds a client-side event and its detail to the response header selected by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ internal readonly struct HtmxLocationOptionsJsonView(string path, HtmxLocationOp
public string? Target => options.Target;

/// <summary>
/// Gets the serialized swap style.
/// Gets the swap expression.
/// </summary>
[JsonConverter(typeof(HtmxSwapJsonConverter))]
public HtmxSwap? Swap => options.Swap;
public string? Swap => options.SwapExpression;

/// <summary>
/// Gets the form field values.
Expand Down
27 changes: 21 additions & 6 deletions src/Ramstack.HtmxToolkit/TagHelpers/HtmxRequestTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public bool? NoHeaders
/// </summary>
/// <remarks>Supported only in HTMX 4.x.</remarks>
[HtmlAttributeName(RequestCacheAttributeName)]
public string? Cache
public HtmxRequestCache? Cache
{
get => _request.Cache;
set => _request.Cache = value;
Expand All @@ -132,7 +132,7 @@ public string? Cache
/// </summary>
/// <remarks>Supported only in HTMX 4.x.</remarks>
[HtmlAttributeName(RequestRedirectAttributeName)]
public string? Redirect
public HtmxRequestRedirect? Redirect
{
get => _request.Redirect;
set => _request.Redirect = value;
Expand Down Expand Up @@ -217,10 +217,10 @@ internal sealed class HtmxRequestData
public bool? NoHeaders { get; set; }

/// <inheritdoc cref="HtmxRequestTagHelper.Cache" />
public string? Cache { get; set; }
public HtmxRequestCache? Cache { get; set; }

/// <inheritdoc cref="HtmxRequestTagHelper.Redirect" />
public string? Redirect { get; set; }
public HtmxRequestRedirect? Redirect { get; set; }

/// <inheritdoc cref="HtmxRequestTagHelper.Referrer" />
public string? Referrer { get; set; }
Expand Down Expand Up @@ -286,10 +286,25 @@ internal readonly struct HtmxRequestDataV4(HtmxRequestData data)
};

/// <inheritdoc cref="HtmxRequestData.Cache" />
public string? Cache => data.Cache;
public string? Cache => data.Cache switch
{
HtmxRequestCache.Default => "default",
HtmxRequestCache.NoStore => "no-store",
HtmxRequestCache.Reload => "reload",
HtmxRequestCache.NoCache => "no-cache",
HtmxRequestCache.ForceCache => "force-cache",
HtmxRequestCache.OnlyIfCached => "only-if-cached",
_ => null
};

/// <inheritdoc cref="HtmxRequestData.Redirect" />
public string? Redirect => data.Redirect;
public string? Redirect => data.Redirect switch
{
HtmxRequestRedirect.Follow => "follow",
HtmxRequestRedirect.Error => "error",
HtmxRequestRedirect.Manual => "manual",
_ => null
};

/// <inheritdoc cref="HtmxRequestData.Referrer" />
public string? Referrer => data.Referrer;
Expand Down
61 changes: 61 additions & 0 deletions tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,67 @@ public void Location_WithOptions_OmitsNullProperties()
Assert.That(json.ContainsKey("swap"), Is.False);
}

[TestCase("outerHTML settle:200ms", HtmxSwap.OuterHtml)]
[TestCase("customSwap settle:200ms", null)]
public void Location_WithSwapExpression_SerializesExpression(string expression, HtmxSwap? expectedStyle)
{
var context = TestHelper.CreateHtmxRequestContext();
var options = new HtmxLocationOptions
{
Swap = HtmxSwap.InnerHtml,
SwapExpression = expression
};

context.Response.Htmx(r => r.Location("/bar", options));

var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString();
var json = JsonHelper.ParseJson(header);

Assert.That(options.Swap, Is.EqualTo(expectedStyle));
Assert.That(json["swap"].GetString(), Is.EqualTo(expression));
}

[Test]
public void Location_WithTypedSwap_ReplacesExpression()
{
var context = TestHelper.CreateHtmxRequestContext();
var options = new HtmxLocationOptions
{
SwapExpression = "innerHTML settle:200ms",
Swap = HtmxSwap.OuterHtml
};

context.Response.Htmx(r => r.Location("/bar", options));

var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString();
var json = JsonHelper.ParseJson(header);

Assert.That(options.SwapExpression, Is.EqualTo("outerHTML"));
Assert.That(json["swap"].GetString(), Is.EqualTo("outerHTML"));
}

[TestCase(false)]
[TestCase(true)]
public void Location_WithClearedSwap_OmitsSwap(bool clearExpression)
{
var context = TestHelper.CreateHtmxRequestContext();
var options = new HtmxLocationOptions { SwapExpression = "outerHTML settle:200ms" };

if (clearExpression)
options.SwapExpression = null;
else
options.Swap = null;

context.Response.Htmx(r => r.Location("/bar", options));

var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString();
var json = JsonHelper.ParseJson(header);

Assert.That(options.Swap, Is.Null);
Assert.That(options.SwapExpression, Is.Null);
Assert.That(json.ContainsKey("swap"), Is.False);
}

[Test]
public void Location_WithOptions_SerializesValuesAndHeaders()
{
Expand Down
Loading
Loading