diff --git a/src/Ramstack.HtmxToolkit/AjaxContext.cs b/src/Ramstack.HtmxToolkit/AjaxContext.cs
deleted file mode 100644
index 3b5a166..0000000
--- a/src/Ramstack.HtmxToolkit/AjaxContext.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-namespace Ramstack.HtmxToolkit;
-
-///
-/// Represents the context for an AJAX request.
-///
-public sealed class AjaxContext
-{
- ///
- /// Gets or sets the path used for the AJAX request.
- ///
- internal string? Path { get; set; }
-
- ///
- /// Gets or sets the source element that initiated the request.
- ///
- public string? Source { get; set; }
-
- ///
- /// Gets or sets the event that triggered the request.
- ///
- public string? Event { get; set; }
-
- ///
- /// Gets or sets the name of the client-side callback function that will handle the response HTML.
- ///
- public string? Handler { get; set; }
-
- ///
- /// Gets or sets the target element into which the response will be swapped.
- ///
- public string? Target { get; set; }
-
- ///
- /// Gets or sets how the response will be swapped relative to the target element.
- ///
- public HtmxSwap? Swap { get; set; }
-
- ///
- /// Gets or sets the values to submit with the request.
- ///
- public object? Values { get; set; }
-
- ///
- /// Gets or sets the headers to include with the request.
- /// Header values must be strings; complex data should be passed as a pre-serialized JSON string.
- ///
- public IDictionary? Headers { get; set; }
-
- ///
- /// Gets or sets a selector used to filter the content to swap from the response.
- ///
- public string? Select { get; set; }
-}
diff --git a/src/Ramstack.HtmxToolkit/AjaxContextWrapper.cs b/src/Ramstack.HtmxToolkit/AjaxContextWrapper.cs
deleted file mode 100644
index ea72b15..0000000
--- a/src/Ramstack.HtmxToolkit/AjaxContextWrapper.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-using System.Text.Json.Serialization;
-
-using Ramstack.HtmxToolkit.Internal;
-
-namespace Ramstack.HtmxToolkit;
-
-///
-/// Represents a wrapper for the class, used for serialization.
-///
-internal readonly struct AjaxContextWrapper
-{
- private readonly AjaxContext _context;
-
- [JsonPropertyName("path")] public string? Path => _context.Path;
- [JsonPropertyName("source")] public string? Source => _context.Source;
- [JsonPropertyName("event")] public string? Event => _context.Event;
- [JsonPropertyName("handler")] public string? Handler => _context.Handler;
- [JsonPropertyName("target")] public string? Target => _context.Target;
- [JsonPropertyName("swap")] public string? Swap => _context.Swap.GetSwapValue();
- [JsonPropertyName("values")] public object? Values => _context.Values;
- [JsonPropertyName("headers")] public IDictionary? Headers => _context.Headers;
- [JsonPropertyName("select")] public string? Select => _context.Select;
-
- ///
- /// Initializes a new instance of the .
- ///
- /// The path of the AJAX request.
- /// The .
- public AjaxContextWrapper(string path, AjaxContext context)
- {
- context.Path = path;
- _context = context;
- }
-}
diff --git a/src/Ramstack.HtmxToolkit/HtmxFieldValues.cs b/src/Ramstack.HtmxToolkit/HtmxFieldValues.cs
new file mode 100644
index 0000000..6e908c2
--- /dev/null
+++ b/src/Ramstack.HtmxToolkit/HtmxFieldValues.cs
@@ -0,0 +1,226 @@
+using System.Collections;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Serialization;
+
+namespace Ramstack.HtmxToolkit;
+
+///
+/// Represents one or more values for a form field submitted with an HTMX request.
+///
+///
+/// A single value can be assigned from a string. Multiple values can be specified with a collection expression.
+///
+[JsonConverter(typeof(HtmxFieldValuesJsonConverter))]
+[CollectionBuilder(typeof(HtmxFieldValues), nameof(Create))]
+public readonly struct HtmxFieldValues : IReadOnlyList
+{
+ private readonly object? _values;
+
+ ///
+ public int Count
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ var values = _values;
+ if (values is null)
+ return 0;
+
+ if (values is string)
+ return 1;
+
+ return Unsafe.As(values).Length;
+ }
+ }
+
+ ///
+ public string this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ var values = _values;
+ if (values is string s)
+ {
+ if (index == 0)
+ return s;
+ }
+ else if (values is not null)
+ {
+ return Unsafe.As(values)[index];
+ }
+
+ return OutOfBounds();
+ }
+ }
+
+ ///
+ /// Gets the underlying value representation.
+ ///
+ internal object? Values => _values;
+
+ ///
+ /// Initializes a new instance of the structure with a single value.
+ ///
+ /// The value to store, or to represent no values.
+ public HtmxFieldValues(string? value) =>
+ _values = value;
+
+ ///
+ /// Initializes a new instance of the structure with the specified values.
+ ///
+ /// The values to store, or to represent no values.
+ ///
+ /// The specified array is stored directly and is not copied.
+ ///
+ public HtmxFieldValues(string[]? values) =>
+ _values = values;
+
+ ///
+ /// Creates an from the specified values.
+ ///
+ /// The values to include.
+ ///
+ /// An containing the specified values.
+ ///
+ public static HtmxFieldValues Create(ReadOnlySpan values)
+ {
+ return values.Length switch
+ {
+ 0 => new HtmxFieldValues([]),
+ 1 => new HtmxFieldValues(values[0]),
+ _ => new HtmxFieldValues(CreateArray(values))
+ };
+ }
+
+ ///
+ /// Converts a string to an .
+ ///
+ /// The value to convert, or to represent no values.
+ public static implicit operator HtmxFieldValues(string? value) =>
+ new(value);
+
+ ///
+ /// Converts an array of strings to an .
+ ///
+ /// The values to convert, or to represent no values.
+ public static implicit operator HtmxFieldValues(string[]? values) =>
+ new(values);
+
+ ///
+ /// Returns an enumerator that iterates through the values.
+ ///
+ ///
+ /// An enumerator for the values.
+ ///
+ public Enumerator GetEnumerator() =>
+ new(this);
+
+ #region IEnumerable: explicit interface implementations
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() =>
+ GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() =>
+ GetEnumerator();
+
+ #endregion
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static string OutOfBounds() =>
+ Array.Empty()[0];
+
+ ///
+ /// Copies the specified read-only span to a new array.
+ ///
+ /// The values to copy.
+ ///
+ /// A new array containing the specified values.
+ ///
+ ///
+ /// The JIT compiler inlines the implementation of
+ /// into its caller, producing a disproportionately large amount of native code at the call site.
+ /// This non-inlined wrapper keeps that implementation out of .
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static string[] CreateArray(ReadOnlySpan s) =>
+ s.ToArray();
+
+ #region Inner type: Enumerator
+
+ ///
+ /// Enumerates the strings represented by an .
+ ///
+ public struct Enumerator : IEnumerator
+ {
+ private readonly string[]? _values;
+ private string? _current;
+ private int _index;
+
+ ///
+ public readonly string Current => _current!;
+
+ ///
+ /// Initializes a new enumerator for the specified .
+ ///
+ /// The value whose strings to enumerate.
+ public Enumerator(HtmxFieldValues value)
+ {
+ if (value.Values is string s)
+ {
+ (_values, _current) = (null, s);
+ }
+ else
+ {
+ (_values, _current) = (Unsafe.As(value.Values), null);
+ }
+
+ _index = 0;
+ }
+
+ ///
+ public bool MoveNext()
+ {
+ var index = _index;
+ if (index < 0)
+ return false;
+
+ var values = _values;
+ if (values is not null)
+ {
+ if ((uint)index < (uint)values.Length)
+ {
+ _index = index + 1;
+ _current = values[index];
+ return true;
+ }
+
+ _index = -1;
+ return false;
+ }
+
+ _index = -1;
+ return _current is not null;
+ }
+
+ ///
+ public readonly void Dispose()
+ {
+ }
+
+ #region IEnumerator: explicit interface implementations
+
+ ///
+ readonly object IEnumerator.Current => Current;
+
+ ///
+ void IEnumerator.Reset() =>
+ throw new NotSupportedException();
+
+ #endregion
+ }
+
+ #endregion
+}
diff --git a/src/Ramstack.HtmxToolkit/HtmxFieldValuesJsonConverter.cs b/src/Ramstack.HtmxToolkit/HtmxFieldValuesJsonConverter.cs
new file mode 100644
index 0000000..98cd0d1
--- /dev/null
+++ b/src/Ramstack.HtmxToolkit/HtmxFieldValuesJsonConverter.cs
@@ -0,0 +1,37 @@
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Ramstack.HtmxToolkit;
+
+///
+/// Represents a JSON converter for .
+///
+internal sealed class HtmxFieldValuesJsonConverter : JsonConverter
+{
+ ///
+ public override HtmxFieldValues Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
+ throw new NotSupportedException();
+
+ ///
+ public override void Write(Utf8JsonWriter writer, HtmxFieldValues value, JsonSerializerOptions options)
+ {
+ var values = value.Values;
+ switch (values)
+ {
+ case string s:
+ writer.WriteStringValue(s);
+ break;
+
+ default:
+ writer.WriteStartArray();
+
+ if (values is not null)
+ foreach (var s in Unsafe.As(values))
+ writer.WriteStringValue(s);
+
+ writer.WriteEndArray();
+ break;
+ }
+ }
+}
diff --git a/src/Ramstack.HtmxToolkit/HtmxHistoryMode.cs b/src/Ramstack.HtmxToolkit/HtmxHistoryMode.cs
index 96c07fa..c5eec00 100644
--- a/src/Ramstack.HtmxToolkit/HtmxHistoryMode.cs
+++ b/src/Ramstack.HtmxToolkit/HtmxHistoryMode.cs
@@ -1,5 +1,3 @@
-using System.Text.Json.Serialization;
-
namespace Ramstack.HtmxToolkit;
///
diff --git a/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs b/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs
new file mode 100644
index 0000000..7d96aad
--- /dev/null
+++ b/src/Ramstack.HtmxToolkit/HtmxLocationOptions.cs
@@ -0,0 +1,73 @@
+using System.Text.Json.Serialization;
+
+namespace Ramstack.HtmxToolkit;
+
+///
+/// Represents the options for an HX-Location request.
+///
+public sealed class HtmxLocationOptions
+{
+ ///
+ /// Gets or sets the path used for the AJAX request.
+ ///
+ /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x.
+ public string? Path { get; internal set; }
+
+ ///
+ /// Gets or sets the source element that initiated the request.
+ ///
+ /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x.
+ public string? Source { get; set; }
+
+ ///
+ /// Gets or sets the target element into which the response will be swapped.
+ ///
+ /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x.
+ public string? Target { get; set; }
+
+ ///
+ /// Gets or sets how the response will be swapped relative to the target element.
+ ///
+ /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x.
+ [JsonConverter(typeof(HtmxSwapJsonConverter))]
+ public HtmxSwap? Swap { get; set; }
+
+ ///
+ /// Gets or sets the form field values to submit with the request.
+ ///
+ /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x.
+ public IDictionary? Values { get; set; }
+
+ ///
+ /// Gets or sets the headers to include with the request.
+ /// Header values must be strings; complex data should be passed as a pre-serialized JSON string.
+ ///
+ /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x.
+ public IDictionary? Headers { get; set; }
+
+ ///
+ /// Gets or sets a selector used to filter the content to swap from the response.
+ ///
+ /// Supported in HTMX 1.9.x, HTMX 2.x, and HTMX 4.x.
+ public string? Select { get; set; }
+
+ ///
+ /// Gets or sets a selector used to select content for out-of-band swaps from the response.
+ ///
+ /// Supported in HTMX 2.x and HTMX 4.x.
+ [JsonPropertyName("selectOOB")]
+ public string? SelectOob { get; set; }
+
+ ///
+ /// Gets or sets the path to push into the browser history.
+ /// Set to false to prevent the URL from being pushed.
+ ///
+ /// Supported in HTMX 2.x and HTMX 4.x.
+ public string? Push { get; set; }
+
+ ///
+ /// Gets or sets the path that replaces the current URL in the browser history.
+ ///
+ /// Supported in HTMX 2.x and HTMX 4.x.
+ public string? Replace { get; set; }
+}
diff --git a/src/Ramstack.HtmxToolkit/HtmxLocationOptionsJsonSerializerContext.cs b/src/Ramstack.HtmxToolkit/HtmxLocationOptionsJsonSerializerContext.cs
new file mode 100644
index 0000000..90e5059
--- /dev/null
+++ b/src/Ramstack.HtmxToolkit/HtmxLocationOptionsJsonSerializerContext.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+
+using Ramstack.HtmxToolkit.Internal;
+
+namespace Ramstack.HtmxToolkit;
+
+///
+/// Provides source-generated JSON serialization metadata for HX-Location options.
+///
+[JsonSourceGenerationOptions(
+ WriteIndented = false,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ GenerationMode = JsonSourceGenerationMode.Default)]
+[JsonSerializable(typeof(HtmxLocationOptions))]
+internal partial class HtmxLocationOptionsJsonSerializerContext : JsonSerializerContext
+{
+ ///
+ /// Initializes the default serializer context with HTML-safe Unicode encoding.
+ ///
+ static HtmxLocationOptionsJsonSerializerContext()
+ {
+ JsonOptions.ConfigureHtmlSafeUnicode(s_defaultOptions);
+ s_defaultContext = new HtmxLocationOptionsJsonSerializerContext(s_defaultOptions);
+ }
+}
diff --git a/src/Ramstack.HtmxToolkit/HtmxResponse.cs b/src/Ramstack.HtmxToolkit/HtmxResponse.cs
index 83cdaa5..dfa1c8d 100644
--- a/src/Ramstack.HtmxToolkit/HtmxResponse.cs
+++ b/src/Ramstack.HtmxToolkit/HtmxResponse.cs
@@ -2,7 +2,6 @@
using System.Text.Json;
using Microsoft.AspNetCore.Http;
-using Microsoft.Extensions.Primitives;
using Ramstack.HtmxToolkit.Internal;
@@ -51,17 +50,19 @@ public HtmxResponse Location(string value) =>
/// Sets the HX-Location header to a client-side redirect that does not do a full page reload.
///
/// The path of the request.
- /// The AJAX context of the request.
+ /// The options for the HX-Location request.
///
/// The current instance.
///
- public HtmxResponse Location(string path, AjaxContext context)
+ public HtmxResponse Location(string path, HtmxLocationOptions options)
{
- return LocationImpl(this, path, context);
+ return LocationImpl(this, path, options);
- static HtmxResponse LocationImpl(HtmxResponse response, string path, AjaxContext context)
+ static HtmxResponse LocationImpl(HtmxResponse response, string path, HtmxLocationOptions options)
{
- var value = JsonSerializer.Serialize(new AjaxContextWrapper(path, context), JsonOptions.PreserveKeyCase);
+ options.Path = path;
+
+ var value = JsonSerializer.Serialize(options, HtmxLocationOptionsJsonSerializerContext.Default.HtmxLocationOptions);
return SetHeader(response, HtmxResponseHeaderNames.Location, value);
}
}
diff --git a/src/Ramstack.HtmxToolkit/Internal/JsonOptions.cs b/src/Ramstack.HtmxToolkit/Internal/JsonOptions.cs
index 4b8e29f..c0fd200 100644
--- a/src/Ramstack.HtmxToolkit/Internal/JsonOptions.cs
+++ b/src/Ramstack.HtmxToolkit/Internal/JsonOptions.cs
@@ -34,14 +34,4 @@ public static void ConfigureHtmlSafeUnicode(JsonSerializerOptions options) =>
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false
};
-
- ///
- /// JSON serializer options that preserve original property and key casing
- /// while ignoring properties with values.
- ///
- public static readonly JsonSerializerOptions PreserveKeyCase = new()
- {
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
- WriteIndented = false
- };
}
diff --git a/src/Ramstack.HtmxToolkit/Properties/CollectionBuilderAttribute.cs b/src/Ramstack.HtmxToolkit/Properties/CollectionBuilderAttribute.cs
new file mode 100644
index 0000000..444806b
--- /dev/null
+++ b/src/Ramstack.HtmxToolkit/Properties/CollectionBuilderAttribute.cs
@@ -0,0 +1,31 @@
+#if !NET8_0_OR_GREATER
+namespace System.Runtime.CompilerServices;
+
+///
+/// Indicates the method used to build a collection from a collection expression.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, Inherited = false)]
+internal sealed class CollectionBuilderAttribute : Attribute
+{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type containing the builder method.
+ /// The name of the builder method.
+ public CollectionBuilderAttribute(Type builderType, string methodName)
+ {
+ BuilderType = builderType;
+ MethodName = methodName;
+ }
+
+ ///
+ /// Gets the type containing the builder method.
+ ///
+ public Type BuilderType { get; }
+
+ ///
+ /// Gets the name of the builder method.
+ ///
+ public string MethodName { get; }
+}
+#endif
diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxFieldValuesTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxFieldValuesTests.cs
new file mode 100644
index 0000000..caee480
--- /dev/null
+++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxFieldValuesTests.cs
@@ -0,0 +1,266 @@
+using System.Collections;
+using System.Text.Json;
+
+namespace Ramstack.HtmxToolkit.Tests;
+
+[TestFixture]
+public class HtmxFieldValuesTests
+{
+ [Test]
+ public void DefaultValue_IsEmpty()
+ {
+ var value = default(HtmxFieldValues);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(value.Count, Is.Zero);
+ Assert.That(value.Values, Is.Null);
+ Assert.That(value, Is.Empty);
+ Assert.That(value, Is.EquivalentTo(Array.Empty()));
+ });
+ }
+
+ [Test]
+ public void Enumeration_DefaultValue_ForeachCompletesWithoutItems()
+ {
+ var values = default(HtmxFieldValues);
+ var list = new List();
+
+ Assert.DoesNotThrow(() =>
+ {
+ foreach (var item in values)
+ list.Add(item);
+ });
+
+ Assert.That(list, Is.Empty);
+ }
+
+ [Test]
+ public void Constructor_SingleValue_UsesStringRepresentation()
+ {
+ var values = new HtmxFieldValues("value");
+ Assert.That(values, Is.EqualTo(["value"]));
+ }
+
+ [Test]
+ public void Constructor_Array_StoresArrayWithoutCopying()
+ {
+ var source = new[] { "first", "second" };
+ var values = new HtmxFieldValues(source);
+
+ source[1] = "changed";
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(values.Count, Is.EqualTo(2));
+ Assert.That(values.Values, Is.SameAs(source));
+ Assert.That(values[1], Is.EqualTo("changed"));
+ Assert.That(values, Is.EqualTo(source));
+ });
+ }
+
+ [Test]
+ public void Constructor_EmptyArray_PreservesArrayRepresentation()
+ {
+ var source = new string [0];
+ var values = new HtmxFieldValues(source);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(values.Count, Is.Zero);
+ Assert.That(values.Values, Is.SameAs(source));
+ Assert.That(values, Is.Empty);
+ });
+ }
+
+ [Test]
+ public void Constructor_NullArray_IsEmpty()
+ {
+ var single = new HtmxFieldValues((string?)null);
+ var multiple = new HtmxFieldValues((string[]?)null);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(single.Count, Is.Zero);
+ Assert.That(multiple.Count, Is.Zero);
+
+ Assert.That(single.Values, Is.Null);
+ Assert.That(multiple.Values, Is.Null);
+
+ Assert.That(single, Is.Empty);
+ Assert.That(multiple, Is.Empty);
+ });
+ }
+
+ [Test]
+ public void Create_EmptySpan_ReturnsEmptyArrayRepresentation()
+ {
+ var values = HtmxFieldValues.Create(ReadOnlySpan.Empty);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(values.Count, Is.Zero);
+ Assert.That(values.Values, Is.TypeOf());
+ Assert.That(values.Values, Is.Empty);
+ });
+ }
+
+ [Test]
+ public void Create_SingleValue_UsesStringRepresentation()
+ {
+ var source = new[] { "single" };
+ var values = HtmxFieldValues.Create(source);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(values.Count, Is.EqualTo(1));
+ Assert.That(values.Values, Is.TypeOf());
+ Assert.That(values[0], Is.EqualTo("single"));
+ Assert.That(values, Is.EqualTo(["single"]));
+ });
+ }
+
+ [Test]
+ public void Create_MultipleValues_CopiesTheSpan()
+ {
+ var source = new[] { "before", "first", "second", "after" };
+ var values = HtmxFieldValues.Create(source.AsSpan());
+ source[1] = "changed";
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(values.Count, Is.EqualTo(4));
+ Assert.That(values.Values, Is.TypeOf());
+ Assert.That(values.Values, Is.Not.SameAs(source));
+ Assert.That(values, Is.EqualTo(["before", "first", "second", "after"]));
+ });
+ }
+
+ [Test]
+ public void CollectionExpressions_SelectCompactRepresentations()
+ {
+ HtmxFieldValues empty = [];
+ HtmxFieldValues single = ["one"];
+ HtmxFieldValues multiple = ["one", "two"];
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(empty.Values, Is.TypeOf());
+ Assert.That(empty, Is.Empty);
+ Assert.That(single.Values, Is.TypeOf());
+ Assert.That(single, Is.EqualTo(["one"]));
+ Assert.That(multiple.Values, Is.TypeOf());
+ Assert.That(multiple, Is.EqualTo(["one", "two"]));
+ });
+ }
+
+ [Test]
+ public void ImplicitConversions_PreserveSourceRepresentations()
+ {
+ var array = new[] { "one", "two" };
+
+ HtmxFieldValues single = "value";
+ HtmxFieldValues multiple = array;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(single.Values, Is.EqualTo("value"));
+ Assert.That(single, Is.EqualTo(["value"]));
+ Assert.That(multiple.Values, Is.SameAs(array));
+ Assert.That(multiple, Is.EqualTo(array));
+ });
+ }
+
+ [Test]
+ public void ImplicitConversions_NullSources_AreEmpty()
+ {
+ HtmxFieldValues single = (string?)null;
+ HtmxFieldValues multiple = (string[]?)null;
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(single.Count, Is.Zero);
+ Assert.That(multiple.Count, Is.Zero);
+
+ Assert.That(single.Values, Is.Null);
+ Assert.That(multiple.Values, Is.Null);
+
+ Assert.That(single, Is.Empty);
+ Assert.That(multiple, Is.Empty);
+ });
+ }
+
+ [TestCase(-1)]
+ [TestCase(1)]
+ public void Indexer_SingleValueInvalidIndex_ThrowsException(int index)
+ {
+ var values = new HtmxFieldValues("value");
+
+ Assert.Throws(() => _ = values[index]);
+ }
+
+ [TestCase(-1)]
+ [TestCase(2)]
+ public void Indexer_ArrayInvalidIndex_ThrowsException(int index)
+ {
+ var values = new HtmxFieldValues(["one", "two"]);
+
+ Assert.Throws(() => _ = values[index]);
+ }
+
+ [Test]
+ public void Enumeration_GenericAndNonGenericInterfaces_ReturnValuesInOrder()
+ {
+ HtmxFieldValues values = ["first", "second", "third"];
+
+ // ReSharper disable once RedundantCast
+ var generic = ((IEnumerable)values).ToArray();
+
+ // ReSharper disable once RedundantCast
+ var nonGeneric = ((IEnumerable)values).Cast().ToArray();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(generic, Is.EqualTo(["first", "second", "third"]));
+ Assert.That(nonGeneric, Is.EqualTo(generic));
+ });
+ }
+
+ [Test]
+ public void Enumerator_AfterSequenceEnds_RemainsCompleted()
+ {
+ var enumerator = new HtmxFieldValues.Enumerator(["first", "second"]);
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(enumerator.MoveNext(), Is.True);
+ Assert.That(enumerator.Current, Is.EqualTo("first"));
+ Assert.That(enumerator.MoveNext(), Is.True);
+ Assert.That(enumerator.Current, Is.EqualTo("second"));
+ Assert.That(enumerator.MoveNext(), Is.False);
+ Assert.That(enumerator.MoveNext(), Is.False);
+ });
+ }
+
+ [Test]
+ public void Enumerator_Reset_ThrowsNotSupportedException()
+ {
+ IEnumerator enumerator = new HtmxFieldValues.Enumerator("value");
+
+ Assert.Throws(enumerator.Reset);
+ }
+
+ [Test]
+ public void JsonSerialization_EmptySingleAndMultipleValues_UsesExpectedShapes()
+ {
+ HtmxFieldValues single = "a\"b";
+ HtmxFieldValues multiple = ["first", "second"];
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(JsonSerializer.Serialize(default(HtmxFieldValues)), Is.EqualTo("[]"));
+ Assert.That(JsonSerializer.Serialize(single), Is.EqualTo("\"a\\u0022b\""));
+ Assert.That(JsonSerializer.Serialize(multiple), Is.EqualTo("[\"first\",\"second\"]"));
+ });
+ }
+}
diff --git a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs
index 0adf4a3..40860b6 100644
--- a/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs
+++ b/tests/Ramstack.HtmxToolkit.Tests/HtmxResponseTests.cs
@@ -13,13 +13,12 @@ public void Location_SetsHeader()
}
[Test]
- public void Location_WithContext_SerializesJson()
+ public void Location_WithOptions_SerializesJson()
{
var context = TestHelper.CreateHtmxRequestContext();
- context.Response.Htmx(r => r.Location("/bar", new AjaxContext
+ context.Response.Htmx(r => r.Location("/bar", new HtmxLocationOptions
{
Source = "button",
- Event = "click",
Target = "#content",
Swap = HtmxSwap.OuterHtml,
Select = "#list"
@@ -30,17 +29,16 @@ public void Location_WithContext_SerializesJson()
Assert.That(json["path"].GetString(), Is.EqualTo("/bar"));
Assert.That(json["source"].GetString(), Is.EqualTo("button"));
- Assert.That(json["event"].GetString(), Is.EqualTo("click"));
Assert.That(json["target"].GetString(), Is.EqualTo("#content"));
Assert.That(json["swap"].GetString(), Is.EqualTo("outerHTML"));
Assert.That(json["select"].GetString(), Is.EqualTo("#list"));
}
[Test]
- public void Location_WithContext_OmitsNullProperties()
+ public void Location_WithOptions_OmitsNullProperties()
{
var context = TestHelper.CreateHtmxRequestContext();
- context.Response.Htmx(r => r.Location("/bar", new AjaxContext()));
+ context.Response.Htmx(r => r.Location("/bar", new HtmxLocationOptions()));
var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString();
var json = JsonHelper.ParseJson(header);
@@ -51,24 +49,49 @@ public void Location_WithContext_OmitsNullProperties()
}
[Test]
- public void Location_WithContext_SerializesHandlerValuesAndHeaders()
+ public void Location_WithOptions_SerializesValuesAndHeaders()
{
var context = TestHelper.CreateHtmxRequestContext();
- context.Response.Htmx(r => r.Location("/bar", new AjaxContext
+ context.Response.Htmx(r => r.Location("/bar", new HtmxLocationOptions
{
- Handler = "handleResponse",
- Values = new Dictionary { ["id"] = 42 },
- Headers = new Dictionary { ["X-Test"] = "abc" }
+ Values = new Dictionary
+ {
+ ["id"] = "42",
+ ["tags"] = ["dotnet", "web"]
+ },
+ Headers = new Dictionary
+ {
+ ["X-Test"] = "abc"
+ }
}));
var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString();
var json = JsonHelper.ParseJson(header);
- Assert.That(json["handler"].GetString(), Is.EqualTo("handleResponse"));
- Assert.That(json["values"].GetProperty("id").GetInt32(), Is.EqualTo(42));
+ Assert.That(json["values"].GetProperty("id").GetString(), Is.EqualTo("42"));
+ Assert.That(json["values"].GetProperty("tags").GetRawText(), Is.EqualTo("[\"dotnet\",\"web\"]"));
Assert.That(json["headers"].GetProperty("X-Test").GetString(), Is.EqualTo("abc"));
}
+ [Test]
+ public void Location_WithOptions_SerializesHistoryAndOutOfBandOptions()
+ {
+ var context = TestHelper.CreateHtmxRequestContext();
+ context.Response.Htmx(r => r.Location("/bar", new HtmxLocationOptions
+ {
+ SelectOob = "#alerts",
+ Push = "false",
+ Replace = "/replaced"
+ }));
+
+ var header = context.Response.Headers[HtmxResponseHeaderNames.Location].ToString();
+ var json = JsonHelper.ParseJson(header);
+
+ Assert.That(json["selectOOB"].GetString(), Is.EqualTo("#alerts"));
+ Assert.That(json["push"].GetString(), Is.EqualTo("false"));
+ Assert.That(json["replace"].GetString(), Is.EqualTo("/replaced"));
+ }
+
[Test]
public void PushUrl_SetsHeader()
{