Provides HTMX integration for ASP.NET Core applications.
Install Ramstack.HtmxToolkit
NuGet package to your project,
use the following command
dotnet add package Ramstack.HtmxToolkit
The library provides a set of classes for working with HttpRequest
.
First off, there's the HttpRequestExtensions
class.
/// <summary>
/// Provides extension methods for the <see cref="HttpRequest"/> class.
/// </summary>
public static class HttpRequestExtensions
{
/// <summary>
/// Determines whether the specified HTTP request is htmx request.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <returns>
/// <c>true</c> if the specified HTTP request is htmx request; otherwise, <c>false</c>.
/// </returns>
public static bool IsHtmxRequest(this HttpRequest request);
/// <summary>
/// Determines whether the specified HTTP request is htmx request.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <param name="headers">When this methods returns, contains the <see cref="HtmxRequestHeaders"/>
/// that provides well-known htmx headers.</param>
/// <returns>
/// <c>true</c> if the specified HTTP request is htmx request; otherwise, <c>false</c>.
/// </returns>
public static bool IsHtmxRequest(this HttpRequest request, out HtmxRequestHeaders headers);
/// <summary>
/// Determines whether the specified HTTP request was made using AJAX
/// instead of a normal navigation.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <returns>
/// <c>true</c> if the specified HTTP request is boosted; otherwise, <c>false</c>.
/// </returns>
public static bool IsHtmxBoosted(this HttpRequest request);
/// <summary>
/// Determines whether the specified HTTP request was made using AJAX
/// instead of a normal navigation.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <param name="headers">When this methods returns, contains the <see cref="HtmxRequestHeaders"/>
/// that provides well-known htmx headers.</param>
/// <returns>
/// <c>true</c> if the specified HTTP request is boosted; otherwise, <c>false</c>.
/// </returns>
public static bool IsHtmxBoosted(this HttpRequest request, out HtmxRequestHeaders headers);
/// <summary>
/// Returns the <see cref="HtmxRequestHeaders"/> that provides well-known htmx headers.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <returns>
/// The <see cref="HtmxRequestHeaders"/>.
/// </returns>
public static HtmxRequestHeaders GetHtmxHeaders(this HttpRequest request);
}
The IsHtmxRequest
method allows you to determine whether the current request is initiated by HTMX.
HttpContext.Request.IsHtmxRequest()
And thus, you can define different scenarios depending on the result, for example:
if (Request.IsHtmxRequest())
return PartialView();
return View();
The overloads of these methods provide access to strongly typed headers set by HTMX:
if (Request.IsHtmxRequest(out var headers))
{
if (headers.HistoryRestoreRequest)
{
...
}
}
In addition, access to strongly typed headers can be obtained by calling GetHtmxHeaders
:
var headers = Request.GetHtmxHeaders();
The full list of headers is presented below:
/// <summary>
/// Represents strongly typed HTMX request headers.
/// </summary>
public readonly struct HtmxRequestHeaders
{
/// <summary>
/// Gets a value indicating whether the request
/// was made using AJAX instead of a normal navigation.
/// </summary>
public bool Boosted { get; }
/// <summary>
/// Gets the current URL of the browser.
/// </summary>
public string? CurrentUrl { get; }
/// <summary>
/// Gets a value indicating whether the request
/// is for history restoration after a miss in the local history cache.
/// </summary>
public bool HistoryRestoreRequest { get; }
/// <summary>
/// Gets the user response to an hx-prompt on the client.
/// </summary>
public string? Prompt { get; }
/// <summary>
/// Gets a value indicating whether the current request is htmx request.
/// </summary>
public bool Request { get; }
/// <summary>
/// Gets the ID of the target element if it exists.
/// </summary>
public string? Target { get; }
/// <summary>
/// Gets the name of the triggered element if it exists.
/// </summary>
public string? TriggerName { get; }
/// <summary>
/// Gets the ID of the triggered element if it exists
/// as indicated by the <c>HX-Trigger</c> header.
/// </summary>
public string? Trigger { get; }
}
Example of usage:
if (Request.GetHtmxHeaders().HistoryRestoreRequests)
{
...
}
The library also provides a set of predefined string constants for request headers,
so you don't have to remember them each time and risk making mistakes in spelling.
You can find them in the HtmxRequestHeaderNames
class.
/// <summary>
/// Defines constants for the well-known names of htmx request headers.
/// For more information, see https://htmx.org/reference/#request_headers
/// </summary>
public static class HtmxRequestHeaderNames
{
/// <summary>
/// The <c>HX-Boosted</c> header indicates whether the request was made using AJAX
/// instead of a normal navigation.
/// </summary>
public const string Boosted = "HX-Boosted";
/// <summary>
/// The <c>HX-Current-URL</c> header contains the current URL of the browser.
/// </summary>
public const string CurrentUrl = "HX-Current-URL";
...
// The list of other constants is omitted for brevity
}
To simplify some manual checks, the library provides an ASP.NET Core result filter that can be applied to an action controller, page handler, or the entire controller:
public class UserController : ControlleBase
{
[HtmxRequest]
public IActionResult UpdateProfile(UserProfile profile)
{
...
}
}
If you are processing boosted requests in a special way, add the Boosted
parameter.
public class UserController : ControlleBase
{
...
[HtmxRequest(Boosted = true)]
public IActionResult UpdateProfile(UserProfile profile)
{
...
}
}
For working with response headers, the library also provides a set of classes.
The first one is the HttpResponseExtension
class with extension methods:
/// <summary>
/// Provides extension methods for the <see cref="HttpResponse"/> class.
/// </summary>
public static class HttpResponseExtensions
{
/// <summary>
/// Returns the <see cref="HtmxResponseHeaders"/> that provides well-known htmx headers.
/// </summary>
/// <param name="response">The HTTP response.</param>
/// <returns>
/// The <see cref="HtmxResponseHeaders"/>.
/// </returns>
public static HtmxResponseHeaders GetHtmxHeaders(this HttpResponse response);
/// <summary>
/// Configures the htmx response headers.
/// </summary>
/// <param name="response">The HTTP response to configure.</param>
/// <param name="configure">The function to configure the htmx response headers.</param>
public static void Htmx(this HttpResponse response, Action<HtmxResponse> configure);
/// <summary>
/// Configures the htmx response headers.
/// </summary>
/// <param name="response">The HTTP response to configure.</param>
/// <param name="configure">The function to configure the htmx response headers.</param>
/// <param name="state">The value to pass to the <paramref name="configure"/>.</param>
public static void Htmx<TState>(this HttpResponse response, Action<HtmxResponse, TState> configure, TState state);
}
The GetHtmxHeaders
method provides access to strongly typed response headers
that control HTMX behavior.
/// <summary>
/// Represents strongly typed HTMX response headers.
/// </summary>
public sealed class HtmxResponseHeaders
{
/// <summary>
/// Gets or sets the <c>HX-Location</c> header to a client-side redirect
/// that does not do a full page reload.
/// </summary>
[MaybeNull]
public string Location { get; set; }
/// <summary>
/// Gets or sets the <c>HX-Push-Url</c> header to push a new URL into the history stack.
/// </summary>
[MaybeNull]
public string PushUrl { get; set; }
...
// The remaining properties are omitted for brevity
}
Just like HtmxRequestHeaderNames
, which consists of predefined string constants for HTMX request headers,
there is a corresponding HtmxResponseHeaderNames
class containing
a list of string constants for HTMX response headers.
/// <summary>
/// Defines constants for the well-known names of htmx response headers.
/// For more information, see https://htmx.org/reference/#response_headers
/// </summary>
public static class HtmxResponseHeaderNames
{
/// <summary>
/// The <c>HX-Location</c> header is used to a client-side redirect that does not do a full page reload.
/// </summary>
public const string Location = "HX-Location";
/// <summary>
/// The <c>HX-Push-Url</c> header is used to push a new URL into the history stack.
/// </summary>
public const string PushUrl = "HX-Push-Url";
/// <summary>
/// The <c>HX-Redirect</c> header is used to client-side redirect to a new location.
/// </summary>
public const string Redirect = "HX-Redirect";
...
// The list of other constants is omitted for brevity
}
However, the most convenient and efficient way is by using one of the provided Htmx
methods
with a callback that accepts HtmxResponse
, allowing you to specify response headers
in a fluent style:
Response.Htmx(h => h
.TriggerEvent(
eventName: "process",
detail: new { Value = ... })
.StopPolling(ShouldStopPolling));
💡 The second Htmx method accepts an additional parameter to avoid unnecessary allocations due to closures:
Response.Htmx(
static (h, stop) => h
.TriggerEvent(
eventName: "process",
detail: new { Value = ... })
.StopPolling(stop),
ShouldStopPolling);
💡 The Htmx
extension methods are also available for IActionResult
, allowing you to write:
return Json(profile).Htmx(h => h.StopPolling(ShouldStopPolling));
In both cases, the headers will be set only in the case of an htmx
request.
In the case of a regular request, the callback passed to the Htmx(this)
method will not be executed,
which allows avoiding unnecessary work.
Some of the response headers can be set declaratively using the HtmxResponseAttribute
,
which is applied to the controller, action, or page.
public class UserController : ControlleBase
{
[HtmxRequest]
[HtmxResponse(
StopPolling = true,
Reswap = HtmxSwap.OuterHtml)]
public IActionResult UpdateProfile(UserProfile profile)
{
...
}
}
💡 If a more complex expression is needed for swap
, for example, innerHTML show:#result:top
,
you can use the Reswap
method in the HtmxResponse
class, which accepts a string.
/// <summary>
/// Sets the <c>HX-Reswap</c> header to specify how the response will be swapped.
/// </summary>
/// <param name="value">The header value to set.</param>
/// <returns>
/// The current <see cref="HtmxResponse"/> instance.
/// </returns>
public HtmxResponse Reswap(string value);
/// <summary>
/// Sets the <c>HX-Reswap</c> header to specify how the response will be swapped.
/// </summary>
/// <param name="value">The header value to set.</param>
/// <returns>
/// The current <see cref="HtmxResponse"/> instance.
/// </returns>
public HtmxResponse Reswap(HtmxSwap value);
And for the HtmxResponseAttribute
, there is the ReswapExpression
property.
/// <summary>
/// Gets or sets the <c>HX-Reswap</c> header that allows to specify how the response will be swapped.
/// </summary>
[MaybeNull]
public string ReswapExpression { get; set; }
/// <summary>
/// Gets or sets the <c>HX-Reswap</c> header that allows to specify how the response will be swapped.
/// </summary>
public HtmxSwap Reswap { get; set; }
allowing you to flexibly configure the swap
header you need.
The library provides 3 tag helpers:
HtmxUrlTagHelper
HtmxHeaderTagHelper
HtmxConfigTagHelper
To make them available in your project, add the @addTagHelper
directive in the Razor view.
@addTagHelper *, Ramstack.HtmxToolkit
To make the tag helpers available globally for the entire application, you should add this line
to the _ViewImports.cshtml
file, which is inherited by all view files by default.
The HtmxUrlTagHelper
allows generating links for HTMX methods similar to how it's done in ASP.NET Core
for generating links, just replace the asp-
prefix with the hx-
prefix.
<div hx-target="this">
<button hx-area="Sessions"
hx-controller="Speaker"
hx-action="Detail"
hx-route-id="@Model.SpeakerId">Show Info</button>
</div>
The following code will be generated:
<div hx-target="this">
<button hx-get="/Sessions/Speaker/Detail/1">Show Info</button>
</div>
By default, if no HTMX method is specified, hx-get
is used. To specify a particular method,
you can choose one from the following attributes: hx-get
, hx-post
, hx-put
, hx-delete
, or hx-patch
.
For instance, in the following example, we use hx-post
:
<div hx-target="this">
<button hx-post
hx-area="Sessions"
hx-controller="Speaker"
hx-action="Detail"
hx-route-id="@Model.SpeakerId">Show Info</button>
</div>
In this case, the following code will be generated:
<div hx-target="this">
<button hx-post="/Sessions/Speaker/Detail/1">Show Info</button>
</div>
The following attributes are also available for obtaining links to a page and a page handler:
<div hx-target="this">
<button hx-page="/Attendee"
hx-page-handler="Profile"
hx-route-attendeeid="1">Attendee Profile</button>
</div>
The following code will be generated:
<div hx-target="this">
<button hx-get="/Attendee?attendeeid=1&handler=Profile">Attendee Profile</button>
</div>
Also, the hx-all-route-data
attribute is available, which accepts a dictionary where
the key is the parameter name, and the value is the parameter value. In the example below,
a dictionary with specific parameters is created, which is then used as the value
of the hx-all-route-data
attribute.
@{
var parameters = new {
category = "science",
pdf = true
};
}
<button hx-target="#result"
hx-action="List"
hx-all-route-data="parameters">Books</a>
The following code will be generated:
<button hx-target="#result" hx-get="/Books/List?category=science&pdf=true">Books</a>
In addition to the examples mentioned, the following properties are also available:
hx-host
hx-protocol
hx-fragment
The htmx
library allows adding custom headers that will be submitted with an AJAX request.
However, since JSON format should be used for this, writing it out manually is not always convenient,
especially considering the need to escape special characters. Fortunately, the library provides
the HtmxHeaderTagHelper
class, which takes care of this and allows specifying headers
in a clearer and more readable format.
<div hx-action="example"
hx-header-Key-1="Value-1"
hx-header-Key-2="Value-2">
Get Some HTML, Including A Custom Header in the Request
</div>
The following code will be generated:
<div hx-get="/home/example"
hx-headers='{"Key-1":"Value-1","Key-2":"Value-2"}'>
Get Some HTML, Including A Custom Header in the Request
</div>
Also, if you have a dictionary with the headers you need,
you can assign them to the hx-all-headers
attribute:
<div hx-action="example"
hx-all-headers="headers">
Get Some HTML, Including A Custom Header in the Request
</div>
The HtmxHeaderTagHelper
will take care of all the remaining work regarding JSON serialization and escaping.
As with hx-headers
, configuring htmx
settings requires a JSON representation.
For working with configuration, the HtmxConfigTagHelper
class is provided.
<!DOCTYPE html>
<html lang="en">
<head>
<meta htmx-config
default-swap-style="HtmxSwap.OuterHtml"
use-template-fragments="true"
scroll-behavior="HtmxScrollBehavior.Smooth"
include-antiforgery-token="true" />
</head>
The following code will be generated:
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="htmx-config"
content='{"defaultSwapStyle":"outerHTML","useTemplateFragments":true,"scrollBehavior":"smooth","antiForgery":{"headerName":"RequestVerificationToken","formFieldName":"__RequestVerificationToken","requestToken":"..."}}' />
</head>
If desired or for the purpose of semantics, you can use htmx-config
as the standalone name of the element:
<htmx-config default-swap-style="HtmxSwap.OuterHtml"
use-template-fragments="true"
scroll-behavior="HtmxScrollBehavior.Smooth"
include-antiforgery-token="true" />
If you have enabled the generation of the Antiforgery token in the configuration
(include-antiforgery-token="true"
), then you need to include a small JavaScript file
that will ensure this token is present in form parameters or headers
and refresh it in a timely manner.
To do this, you can directly include the contents of the JavaScript file on the page:
<script>
@Html.HtmxAntiforgeryScript()
</script>
Alternatively, to retrieve the debug version of the script,
you can pass the debug parameter with a value of true
:
<script>
@Html.HtmxAntiforgeryScript(debug: true)
</script>
The debug
parameter determines which version will be included. By default, the minimized version
of the script will be returned, which weighs very little and takes up approximately 520 bytes.
The method returns a pre-initialized HtmlString
with the script content,
so there will be no unnecessary conversions and allocations every time it's used.
Alternatively, you can register the corresponding endpoint for the script by calling:
app.UseAuthorization();
...
app.MapHtmxAntiforgeryScript();
app.MapControllers();
By default, the registered path is mapped to /htmxtoolkit/[sha1-hash]
,
where [sha1-hash] represents a precalculated hash of the script content.
This approach eliminates the need to worry about cache invalidating
when updating the script in the future as the hash automatically changes
when the script content is modified.
If you want to change the path to your own, specify this path in the parameter.
app.MapHtmxAntiforgeryScript("/my-path");
Now, include it on the page.
<script src="@Html.HtmxAntiforgeryScriptPath()"></script>
Alternatively, to retrieve the debug version of the script, you can pass the debug
parameter
with a value of true
, which instructs to include a query parameter ?debug
in the path.
The presence of this parameter determines the loading of the debug version:
<script src="@Html.HtmxAntiforgeryScriptPath(debug: true)"></script>
The debug
parameter determines whether to load the minimized version (used by default)
or the debug version of the script.
Add [DisallowNull]
attribute to Reswap
property to disallow null input
- Add
AjaxContext
to align with the capabilities provided by htmx - Add method overloads for
PushUrl
andReplaceUrl
that prevents URL changes (PreventPushUrl
/PreventReplaceUrl
) - Add support "htmx-config" element as a standalone HTML element
- Add overloads for IsHtmxRequest and IsHtmxBoosted methods enabling retrieval of htmx request headers
- Improve HtmxRequestAttribute
This package is released under the MIT License.