Skip to content

Commit

Permalink
feat: adds missing reswap modifier and adds reswap builder along with…
Browse files Browse the repository at this point in the history
… StopPolling

This commit introduces several enhancements to the `HtmxResponse` class. It adds a new optional parameter `modifier` to the `Reswap` method, allowing for more flexible response swapping. A new overload of the `Reswap` method is also added that takes a `SwapStyleBuilder` object as an argument.

A new method, `StopPolling`, has been introduced which sets the response code to stop polling.

Two new classes, `HtmxStatusCodes` and `SwapStyleBuilder`, have been created. The former provides status codes specific to HTMX responses while the latter aids in constructing swap style command strings for HTMX responses.

Additionally, a new enum called `ScrollDirection` has been added for specifying scroll direction values.

refactor: Add Reswap method and refactor SwapStyleBuilder

Added a new Reswap method to the HtmxResponse class, allowing for more flexible response swapping. Refactored the SwapStyleBuilder class to use an OrderedDictionary for storing modifiers instead of a List, improving performance and readability. Also added nullability support for the SwapStyle property in SwapStyleBuilder. Added comprehensive unit tests for these changes in SwapStyleBuilderTests.cs.

feat: Add ShowNone method to SwapStyleBuilder

A new method, ShowNone, has been added to the SwapStyleBuilder class. This method turns off scrolling after a swap and allows for chaining with other methods in the class. Corresponding unit test has also been added to ensure that it returns the correct value.
  • Loading branch information
tanczosm committed Apr 30, 2024
1 parent 494ecfa commit 9cba39e
Show file tree
Hide file tree
Showing 5 changed files with 449 additions and 2 deletions.
43 changes: 41 additions & 2 deletions src/Htmxor/Http/HtmxResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,25 @@ public HtmxResponse ReplaceUrl(string url)
return this;
}

/// <summary>
/// Allows you to specify how the response will be swapped.
/// </summary>
/// <param name="modifier">The hx-swap attributes supports modifiers for changing the behavior of the swap.</param>
/// <returns>This <see cref="HtmxResponse"/> object instance.</returns>
public HtmxResponse Reswap(string modifier)
{
headers[HtmxResponseHeaderNames.Reswap] = modifier;

return this;
}

/// <summary>
/// Allows you to specify how the response will be swapped.
/// </summary>
/// <param name="swapStyle"></param>
/// <param name="modifier">The hx-swap attributes supports modifiers for changing the behavior of the swap.</param>
/// <returns>This <see cref="HtmxResponse"/> object instance.</returns>
public HtmxResponse Reswap(SwapStyle swapStyle)
public HtmxResponse Reswap(SwapStyle swapStyle, string? modifier = null)
{
var style = swapStyle switch
{
Expand All @@ -152,11 +165,26 @@ public HtmxResponse Reswap(SwapStyle swapStyle)
_ => throw new SwitchExpressionException(swapStyle),
};

headers[HtmxResponseHeaderNames.Reswap] = style;
var value = modifier != null ? $"{style} {modifier}" : style;

headers[HtmxResponseHeaderNames.Reswap] = value;

return this;
}

/// <summary>
/// Allows you to specify how the response will be swapped.
/// </summary>
/// <param></param>
/// <param name="swapStyle"></param>
/// <returns></returns>
public HtmxResponse Reswap(SwapStyleBuilder swapStyle)
{
var (style, modifier) = swapStyle.Build();

return style is null ? Reswap(modifier) : Reswap((SwapStyle)style, modifier);
}

/// <summary>
/// A CSS selector that updates the target of the content update to a different element on the page.
/// </summary>
Expand All @@ -181,6 +209,17 @@ public HtmxResponse Reselect(string selector)
return this;
}

/// <summary>
/// Sets response code to stop polling
/// </summary>
/// <returns></returns>
public HtmxResponse StopPolling()
{
context.Response.StatusCode = HtmxStatusCodes.StopPolling;

return this;
}

/// <summary>
/// Allows you to trigger client-side events.
/// </summary>
Expand Down
6 changes: 6 additions & 0 deletions src/Htmxor/Http/HtmxStatusCodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Htmxor.Http;

public static class HtmxStatusCodes
{
public static readonly int StopPolling = 286;
}
192 changes: 192 additions & 0 deletions src/Htmxor/Http/SwapStyleBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
using System.Collections;
using System.Collections.Specialized;

namespace Htmxor.Http;

/// <summary>
/// A builder class for constructing a swap style command string for HTMX responses.
/// </summary>
public class SwapStyleBuilder
{
private readonly SwapStyle? style;
private readonly OrderedDictionary modifiers = new();

/// <summary>
/// Initializes a new instance of the SwapStyleBuilder with a specified swap style.
/// </summary>
/// <param name="style">The initial swap style to be applied.</param>
public SwapStyleBuilder(SwapStyle? style = null)
{
this.style = style;
}

/// <summary>
/// Adds a delay to the swap operation.
/// </summary>
/// <param name="span">The time span to delay the swap.</param>
/// <returns>The SwapStyleBuilder instance for chaining.</returns>
public SwapStyleBuilder After(TimeSpan span)
{
AddModifier("swap", span.TotalMilliseconds < 1000 ? $"{span.TotalMilliseconds}ms" : $"{span.TotalSeconds}s");

return this;
}

/// <summary>
/// Specifies the direction to scroll the page after the swap.
/// </summary>
/// <param name="direction">The scroll direction.</param>
/// <returns>The SwapStyleBuilder instance for chaining.</returns>
public SwapStyleBuilder Scroll(ScrollDirection direction)
{
switch (direction)
{
case ScrollDirection.Top:
AddModifier("scroll", "top");
break;
case ScrollDirection.Bottom:
AddModifier("scroll", "bottom");
break;
}

return this;
}

/// <summary>
/// Determines whether to ignore the document title in the swap response.
/// </summary>
/// <param name="ignore">Whether to ignore the title.</param>
/// <returns>The SwapStyleBuilder instance for chaining.</returns>
public SwapStyleBuilder IgnoreTitle(bool ignore)
{
AddModifier("ignoreTitle", ignore);

return this;
}

/// <summary>
/// Enables or disables transition effects for the swap.
/// </summary>
/// <param name="show">Whether to show transition effects.</param>
/// <returns>The SwapStyleBuilder instance for chaining.</returns>
public SwapStyleBuilder Transition(bool show)
{
AddModifier("transition", show);

return this;
}

/// <summary>
/// Sets whether to focus and scroll to the swapped content.
/// </summary>
/// <param name="scroll">Whether to scroll to the focus element.</param>
/// <returns>The SwapStyleBuilder instance for chaining.</returns>
public SwapStyleBuilder FocusScroll(bool scroll)
{
AddModifier("focus-scroll", scroll);

return this;
}

/// <summary>
/// Specifies a CSS selector to dynamically target for the swap operation.
/// </summary>
/// <param name="selector">The CSS selector of the target element.</param>
/// <param name="direction">The scroll direction after swap.</param>
/// <returns>The SwapStyleBuilder instance for chaining.</returns>
public SwapStyleBuilder ShowOn(string selector, ScrollDirection direction)
{
switch (direction)
{
case ScrollDirection.Top:
AddModifier("show", $"{selector}:top");
break;
case ScrollDirection.Bottom:
AddModifier("show", $"{selector}:bottom");
break;
}

return this;
}

/// <summary>
/// Specifies that the swap should show in the window, with an optional scroll direction.
/// </summary>
/// <param name="direction">The direction to scroll the window after the swap.</param>
/// <returns>The SwapStyleBuilder instance for chaining.</returns>
public SwapStyleBuilder ShowWindow(ScrollDirection direction)
{
switch (direction)
{
case ScrollDirection.Top:
AddModifier("show", $"window:top");
break;
case ScrollDirection.Bottom:
AddModifier("show", $"window:bottom");
break;
}

return this;
}

/// <summary>
/// Turns off scrolling after swap
/// </summary>
/// <returns>The SwapStyleBuilder instance for chaining.</returns>
public SwapStyleBuilder ShowNone()
{
AddModifier("show", "none");

return this;
}

/// <summary>
/// Builds the swap style command string with all specified modifiers.
/// </summary>
/// <returns>A tuple containing the SwapStyle and the constructed command string.</returns>
internal (SwapStyle?, string) Build()
{
var value = string.Empty;

if (modifiers.Count > 0)
{
value = modifiers.Cast<DictionaryEntry>()
.Select(entry => $"{entry.Key}:{entry.Value}")
.Aggregate((current, next) => $"{current} {next}");
}

return (style, value);
}

/// <summary>
/// Adds a modifier to modifiers, overriding existing values if present
/// </summary>
/// <param name="modifier"></param>
/// <param name="options"></param>
private void AddModifier(string modifier, string options)
{
if (modifiers.Contains(modifier))
modifiers.Remove(modifier);

modifiers.Add(modifier, options);
}

private void AddModifier(string modifier, bool option) => AddModifier(modifier, option.ToString().ToLowerInvariant());
}

/// <summary>
/// Extension methods for the SwapStyle enum to facilitate building swap style commands.
/// </summary>
public static class SwapStyleBuilderExtension
{
// Each method below returns a SwapStyleBuilder instance initialized with the respective SwapStyle
// and applies the specified modifier to it. This allows for fluent configuration of swap style commands.

public static SwapStyleBuilder After(this SwapStyle style, TimeSpan span) => new SwapStyleBuilder(style).After(span);
public static SwapStyleBuilder Scroll(this SwapStyle style, ScrollDirection direction) => new SwapStyleBuilder(style).Scroll(direction);
public static SwapStyleBuilder IgnoreTitle(this SwapStyle style, bool ignore) => new SwapStyleBuilder(style).IgnoreTitle(ignore);
public static SwapStyleBuilder Transition(this SwapStyle style, bool show) => new SwapStyleBuilder(style).Transition(show);
public static SwapStyleBuilder FocusScroll(this SwapStyle style, bool scroll) => new SwapStyleBuilder(style).FocusScroll(scroll);
public static SwapStyleBuilder ShowOn(this SwapStyle style, string selector, ScrollDirection direction) => new SwapStyleBuilder(style).ShowOn(selector, direction);
public static SwapStyleBuilder ShowWindow(this SwapStyle style, ScrollDirection direction) => new SwapStyleBuilder(style).ShowWindow(direction);
}
10 changes: 10 additions & 0 deletions src/Htmxor/ScrollDirection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Htmxor;

/// <summary>
/// Direction values for scroll and show modifier methods
/// </summary>
public enum ScrollDirection
{
Top,
Bottom
}
Loading

0 comments on commit 9cba39e

Please sign in to comment.