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 @@ -9,7 +9,8 @@
<htmx-config
include-antiforgery-token="true"
methods-that-use-url-params="[HttpVerb.Get, HttpVerb.Delete]"
default-swap-style="HtmxSwap.InnerHtml">
default-swap-style="HtmxSwap.InnerHtml"
no-swap="@(["204", "304", "4xx", "5xx"])">
<response-handling code="204" swap="false"/>
<response-handling code="422" swap="true"/>
<response-handling code="[23].." swap="true"/>
Expand Down
62 changes: 46 additions & 16 deletions src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,61 @@
document._r_htmx ||= ((document, htmx) => {
document.addEventListener("htmx:afterOnLoad", e => {
if (e.detail.boosted) {
const html = new DOMParser().parseFromString(e.detail.xhr.responseText, "text/html");
const meta = html.querySelector("meta[name='htmx-config']");
const listen = (type, listener) => {
document.addEventListener(type, listener);
};

meta && (htmx.config.antiForgery = JSON.parse(meta.content).antiForgery);
}
});

document.addEventListener("htmx:configRequest", e => {
if (!/^get$/i.test(e.detail.verb)) {
const add_antiforgery = (method, headers, parameters) => {
if (!/^get$/i.test(method)) {
const {
headerName,
formFieldName,
requestToken
} = htmx.config.antiForgery ?? {};

if (requestToken && !e.detail.parameters[formFieldName]) {
headerName
? e.detail.headers[headerName] = requestToken
: e.detail.parameters[formFieldName] = requestToken;
if (requestToken) {
if (!parameters.has?.(formFieldName) && !parameters[formFieldName])
{
if (headerName) {
headers[headerName] = requestToken;
}
else
{
parameters.set
? parameters.set(formFieldName, requestToken)
: parameters[formFieldName] = requestToken;
}
}
}
}
};

const update_antiforgery = content => {
let html = new DOMParser().parseFromString(content || "", "text/html");
let meta = html.querySelector("meta[name='htmx-config']");
meta && (htmx.config.antiForgery = JSON.parse(meta.content).antiForgery);
};

listen("htmx:afterOnLoad", e => {
let detail = e.detail;
detail.boosted && update_antiforgery(detail.xhr.responseText);
});

listen("htmx:after:request", e => {
let ctx = e.detail.ctx;
ctx.boosted && update_antiforgery(ctx.text);
});

listen("htmx:configRequest", e => {
let detail = e.detail;
add_antiforgery(detail.verb, detail.headers, detail.parameters);
});

listen("htmx:config:request", e => {
let request = e.detail.ctx.request;
add_antiforgery(request.method, request.headers, request.body);
});

document.addEventListener("rs:events", e => {
for (let kvp of e.detail.value) {
listen("rs:events", e => {
for (let kvp of e.detail.value || e.detail) {
htmx.trigger(e.target, kvp.key, kvp.value);
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/Ramstack.HtmxToolkit/Assets/htmx-toolkit.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions src/Ramstack.HtmxToolkit/Builder/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Microsoft.Extensions.DependencyInjection;

namespace Ramstack.HtmxToolkit.Builder;

/// <summary>
/// Provides registration methods for HTMX Toolkit services.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registers HTMX Toolkit configuration and its startup configuration cache.
/// </summary>
/// <param name="services">The service collection.</param>
/// <param name="configure">An optional delegate used to configure HTMX Toolkit.</param>
/// <returns>
/// The same service collection.
/// </returns>
public static IServiceCollection AddHtmxToolkit(this IServiceCollection services, Action<HtmxToolkitOptions>? configure = null)
{
services.AddOptions<HtmxToolkitOptions>();

if (configure is not null)
services.Configure(configure);

return services;
}
}
31 changes: 31 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxFetchMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Ramstack.HtmxToolkit;

/// <summary>
/// Specifies the request mode used by HTMX.
/// </summary>
/// <remarks>
/// <para>In HTMX 4.x this is passed as the <c>mode</c> option of the Fetch API.</para>
/// <para>
/// In HTMX 1.x and 2.x (compatibility mode) this maps to the <c>selfRequestsOnly</c>
/// boolean configuration option, where <see cref="SameOrigin"/> yields
/// <see langword="true" /> and any other value yields <see langword="false" />.
/// </para>
/// </remarks>
public enum HtmxFetchMode
{
/// <summary>
/// Allows requests only to the current origin.
/// </summary>
SameOrigin,

/// <summary>
/// Allows cross-origin requests using CORS.
/// </summary>
Cors,

/// <summary>
/// Allows restricted cross-origin requests that produce opaque responses.
/// Opaque responses cannot normally be swapped by HTMX.
/// </summary>
NoCors
}
24 changes: 24 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxHistoryMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Text.Json.Serialization;

namespace Ramstack.HtmxToolkit;

/// <summary>
/// Specifies how HTMX history restoration is handled.
/// </summary>
public enum HtmxHistoryMode
{
/// <summary>
/// Enables history snapshots and restoration.
/// </summary>
Enabled,

/// <summary>
/// Disables HTMX history support.
/// </summary>
Disabled,

/// <summary>
/// Reloads the page when restoring history in HTMX 4. HTMX 1 and 2 treat this as <see cref="Enabled"/>.
/// </summary>
Reload
}
54 changes: 54 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxHistoryModeJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Ramstack.HtmxToolkit;

/// <summary>
/// Represents a <see cref="JsonConverter{T}"/> for <see cref="HtmxHistoryMode"/> values
/// written in the format expected by the <c>history</c> configuration option:
/// <see cref="HtmxHistoryMode.Enabled"/> and <see cref="HtmxHistoryMode.Disabled"/>
/// are written as booleans, while any other value is written as its lowercase
/// string representation (e.g. "reload").
/// </summary>
/// <remarks>
/// Unlike other enum values, which are serialized as strings, this one requires a custom
/// converter: <see cref="HtmxHistoryMode.Enabled"/> and <see cref="HtmxHistoryMode.Disabled"/>
/// must be written as actual JSON booleans rather than strings, since otherwise htmx
/// would not recognize them.
/// </remarks>
internal sealed class HtmxHistoryModeJsonConverter : JsonConverter<HtmxHistoryMode?>
{
/// <summary>
/// Pre-encoded "reload" text; encoding it once as a static field avoids
/// repeated UTF-8 encoding overhead on each serialization.
/// </summary>
private static readonly JsonEncodedText s_reload = JsonEncodedText.Encode("reload");

/// <inheritdoc />
public override HtmxHistoryMode? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
throw new NotSupportedException();

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, HtmxHistoryMode? value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
}
else
{
switch (value.GetValueOrDefault())
{
case HtmxHistoryMode.Enabled:
writer.WriteBooleanValue(true);
break;
case HtmxHistoryMode.Disabled:
writer.WriteBooleanValue(false);
break;
default:
writer.WriteStringValue(s_reload);
break;
}
}
}
}
23 changes: 23 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxRequestCredentials.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Ramstack.HtmxToolkit;

/// <summary>
/// Specifies the credentials mode for an HTMX request.
/// </summary>
public enum HtmxRequestCredentials
{
/// <summary>
/// Sends credentials only when the request targets the current origin.
/// </summary>
SameOrigin,

/// <summary>
/// Always sends credentials with the request.
/// </summary>
Include,

/// <summary>
/// Never sends credentials with the request.
/// </summary>
/// <remarks>Supported only in HTMX 4.x.</remarks>
Omit
}
20 changes: 20 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxSwap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,26 @@ public enum HtmxSwap
/// </summary>
OuterHtml,

/// <summary>
/// Morphs the inner HTML of the target element.
/// </summary>
InnerMorph,

/// <summary>
/// Morphs the target element itself.
/// </summary>
OuterMorph,

/// <summary>
/// Synchronizes the target element with the response.
/// </summary>
OuterSync,

/// <summary>
/// Replaces the text content of the target element.
/// </summary>
TextContent,

/// <summary>
/// Inserts the response before the target element.
/// </summary>
Expand Down
22 changes: 22 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxTargetVersion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Ramstack.HtmxToolkit;

/// <summary>
/// Specifies the HTMX major version targeted by generated markup.
/// </summary>
public enum HtmxTargetVersion
{
/// <summary>
/// HTMX 1.x.
/// </summary>
V1,

/// <summary>
/// HTMX 2.x.
/// </summary>
V2,

/// <summary>
/// HTMX 4.x.
/// </summary>
V4
}
12 changes: 12 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxToolkitOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Ramstack.HtmxToolkit;

/// <summary>
/// Configures services provided by the HTMX toolkit.
/// </summary>
public sealed class HtmxToolkitOptions
{
/// <summary>
/// Gets or sets the HTMX major version used for version-sensitive generated markup.
/// </summary>
public HtmxTargetVersion TargetVersion { get; set; } = HtmxTargetVersion.V2;
}
31 changes: 31 additions & 0 deletions src/Ramstack.HtmxToolkit/HtmxTriggerSpecsCacheJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Ramstack.HtmxToolkit;

/// <summary>
/// Represents a <see cref="JsonConverter{T}"/> that serializes the <c>triggerSpecsCache</c>
/// configuration option from its boolean form: <see langword="true" /> is written as an empty
/// JSON object (<c>{}</c>), instructing htmx to use a never-clearing trigger specification cache,
/// while <see langword="false" /> and <see langword="null" /> are written as JSON null.
/// </summary>
internal sealed class HtmxTriggerSpecsCacheJsonConverter : JsonConverter<bool?>
{
/// <inheritdoc />
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
throw new NotSupportedException();

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options)
{
if (value.GetValueOrDefault())
{
writer.WriteStartObject();
writer.WriteEndObject();
}
else
{
writer.WriteNullValue();
}
}
}
Loading
Loading