HTMX is a JavaScript library that allows you to add AJAX capabilities directly to HTML using HTML attributes. The library is based on two fundamental ideas:
Allow any HTML element to make an HTTP request.
Use the response HTML to update the element.
A basic example might be:
<button hx-get="/data" hx-trigger="click" hx-swap="outerHTML">
Get
</button>
This button will:
Make an HTTP GET request to
/data
when clicked.Replace itself with the response HTML.
But why should one consider HTMX when several front-end frameworks are available today? Here are some key points to take into consideration:
Simplicity: HTMX allows developers to build interactive web apps using simple HTML attributes instead of JavaScript code.
Performance: Since HTMX relies mainly on HTML (and minimal JavaScript), pages often load faster and use less memory.
Learning Curve: Since HTMX works directly with HTML, it has a lower learning curve for developers who are more comfortable with HTML/CSS.
Server-Side Rendering: HTMX works with any server-side technology. It allows developers to work in their respective backend languages.
Hello World
Every new technology always starts with the classic "hello world" example. Run the following commands to set up the solution:
dotnet new web -o TodoApi
dotnet new sln -n Htmx
dotnet sln add --in-root TodoApi
In the TodoApi
project, and add the HtmlResult.cs
file with the following content:
using System.Net.Mime;
using System.Text;
namespace TodoApi;
public class HtmlResult : IResult
{
private readonly string _html;
public HtmlResult(string html)
{
_html = html;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
return httpContext.Response.WriteAsync(_html);
}
}
This class will be used to return HTML from our Minimal API endpoints. Go to the Program.cs
file and update the content as follows:
using Microsoft.AspNetCore.Html;
using TodoApi;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/hello-world", () => new HtmlResult(@"<!doctype html>
<html>
<head>
<meta charset=""UTF-8"">
<meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
<script src=""https://unpkg.com/htmx.org@1.9.6"" integrity=""sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni"" crossorigin=""anonymous""></script>
</head>
<body>
<button hx-post=""/hello-world"" hx-swap=""outerHTML"">Get time</button>
</body>
</html>
"));
app.MapPost("/hello-world", () => new HtmlResult($@"<div>Hello World {DateTimeOffset.UtcNow}</div>"));
app.Run();
There are two endpoints in our API. The first one returns the initial HTML document, which includes a reference to the HTMX library via CDN. Within the body, there is a button with the HTMX attributes (to be explained later) that instructs the browser to make an HTTP POST request to /hello-world
when the button is clicked and then replaces the button with the response. The second endpoint returns only a div tag containing the message. To see this in action, run the application and navigate to http://localhost:5179/hello-world.
Attributes
The primary attributes used in HTMX are designed for making AJAX requests:
hx-get: Issues a GET request to the given URL.
hx-post: Issues a POST request to the given URL.
hx-put: Issues a PUT request to the given URL.
hx-patch: Issues a PATCH request to the given URL.
hx-delete: Issues a DELETE request to the given URL.
The following attributes are most commonly used. For more information, please visit the official website here.
hx-trigger
The hx-trigger attribute enables us to define what initiates an AJAX request. By default, there is no need to specify the attribute when the following triggers are used:
input
,textarea
&select
are triggered on thechange
event.form
is triggered on thesubmit
event.everything else is triggered by the
click
event.
A trigger value can be one of the following:
An event name.
A polling definition.
A comma-separated list of such events
hx-target
The hx-target
attribute enables us to target a different element for swapping, rather than the one initiating the AJAX request. The value of this attribute can be:
A CSS query selector of the element to target.
this
indicates that the element with thehx-target
attribute is the target itself.closest <CSS selector>
finds the closest ancestor element or itself, that matches the given CSS selector.find <CSS selector>
finds the first child descendant element that matches the given CSS selector.next <CSS selector>
scans the DOM forward for the first element that matches the given CSS selector.previous <CSS selector>
scans the DOM backward for the first element that matches the given CSS selector.
hx-swap
The hx-swap
attribute allows you to specify how the response will be swapped relative to the target of an AJAX request. The possible values of this attribute are:
innerHTML
: Puts the content inside the target element. The default option.outerHTML
: Replace the entire target element with the response.beforebegin
: Insert the content before the target in the parent element.afterbegin
: Insert the response before the first child of the target element.beforeend
: Insert the response after the last child of the target element.afterend
: Insert the response after the target in the parent element.delete
: Deletes the target element regardless of the response.none
: Does not append content from the response.
Todo App
Let's build an application to see all these concepts in action. To build HTML in .NET we can use the HtmlContentBuilder
class rather than using a simple string. This has several benefits:
Parameterizable: Since
HtmlContentBuilder
uses method calls, you can pass parameters to those methods to dynamically build up HTML.Supports Encoding:
HtmlContentBuilder
will automatically HTML encode any unencoded strings you append.Composable: We can compose multiple
HtmlContentBuilder
instances to build up more complex HTML.Easier to Maintain: Building HTML as a single string can become difficult to maintain as the HTML grows more complex.
HtmlContentBuilder
allows you to build up the HTML incrementally using method calls.
Let's create a new IResult
implementation based on the IHtmlContent
interface:
using Microsoft.AspNetCore.Html;
using System.Net.Mime;
using System.Text.Encodings.Web;
using System.Text;
namespace TodoApi;
public class HtmlContentResult : IResult
{
private readonly IHtmlContent _htmlContent;
public HtmlContentResult(IHtmlContent htmlContent)
{
_htmlContent = htmlContent;
}
public Task ExecuteAsync(HttpContext httpContext)
{
httpContext.Response.ContentType = MediaTypeNames.Text.Html;
using (var writer = new StringWriter())
{
_htmlContent.WriteTo(writer, HtmlEncoder.Default);
var html = writer.ToString();
httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(html);
return httpContext.Response.WriteAsync(html);
}
}
}
Create a TodoItem.cs
file to store our model:
namespace TodoApi;
public class TodoItem
{
public Guid Id { get; set; }
public bool IsCompleted { get; set; }
public string? Name { get; set; }
};
Create a Components.cs
class to contain all of our HTML builders:
using Microsoft.AspNetCore.Html;
namespace TodoApi;
public static class Components
{
public static IHtmlContent Document(IHtmlContentContainer children)
{
var builder = new HtmlContentBuilder();
builder.AppendHtml("<!doctype html>");
builder.AppendHtml("<html>");
builder.AppendHtml("<head>");
builder.AppendHtml("<meta charset=\"UTF-8\">");
builder.AppendHtml("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
builder.AppendHtml("<script src=\"https://unpkg.com/htmx.org@1.9.6\" integrity=\"sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni\" crossorigin=\"anonymous\"></script>");
builder.AppendHtml("</head>");
builder.AppendHtml(children);
builder.AppendHtml("</html>");
return builder;
}
public static IHtmlContent TodoList(IEnumerable<TodoItem> todoItems)
{
var builder = new HtmlContentBuilder();
builder.AppendHtml("<div>");
foreach (var todoItem in todoItems)
{
builder.AppendHtml(TodoItem(todoItem));
}
builder.AppendHtml("</div>");
return builder;
}
public static IHtmlContent TodoItem(TodoItem todoItem)
{
var builder = new HtmlContentBuilder();
builder.AppendHtml("<div>");
builder.AppendHtml("<p>");
builder.AppendFormat("{0}", todoItem.Name);
builder.AppendHtml("</p>");
builder.AppendHtml("</div>");
return builder;
}
}
Navigate to the Program.cs
file and add the following code:
var db = new List<TodoItem>()
{
new TodoItem() { Id = Guid.NewGuid(), Name = "abc", IsCompleted = true },
new TodoItem() { Id = Guid.NewGuid(), Name = "123", IsCompleted = false },
};
app.MapGet("/", () =>
{
var builder = new HtmlContentBuilder();
builder.AppendHtml("<body hx-get=\"/todos\" hx-trigger=\"load\" hx-swap=\"innerHTML\">");
builder.AppendHtml("</body>");
return new HtmlContentResult(Components.Document(builder));
});
app.MapGet("/todos", () => new HtmlContentResult(Components.TodoList(db)));
The first endpoint renders the initial HTML document, while the second returns a list of items. During the body tag's load event (hx-trigger="load"
), HTMX makes a request (hx-get="/todos"
) and inserts the response inside the element (hx-swap="innerHTML"
). Let's add the features to toggle and delete items. Update the TodoItem
component as follows:
public static IHtmlContent TodoItem(TodoItem todoItem)
{
var isCompleted = string.Empty;
if (todoItem.IsCompleted)
{
isCompleted = "checked";
}
var builder = new HtmlContentBuilder();
builder.AppendHtml("<div>");
builder.AppendHtml("<p>");
builder.AppendFormat("{0}", todoItem.Name);
builder.AppendHtml("</p>");
builder.AppendHtml($"<input type=\"checkbox\" {isCompleted} hx-post=\"/todos/{todoItem.Id}/toggle\" hx-target=\"closest div\" hx-swap=\"outerHTML\"/>");
builder.AppendHtml($"<button hx-delete=\"/todos/{todoItem.Id}\" hx-target=\"closest div\" hx-swap=\"outerHTML\">X</button>");
builder.AppendHtml("</div>");
return builder;
}
During the change event of the checkbox input, HTMX initiates a request (hx-post="/todos/{todoItem.Id}/toggle"
) and replaces the element (hx-target="closest div"
) with the response (hx-swap="outerHTML"
), updating the list. In the same line, the click event performs a similar action; however, since the response is empty, the item is removed from the list. In the Program.cs
file, add these two endpoints:
app.MapPost("/todos/{id}/toggle", (string id) =>
{
var todo = db.First(t => t.Id.ToString() == id);
todo.IsCompleted = !todo.IsCompleted;
return new HtmlContentResult(Components.TodoItem(todo));
});
app.MapDelete("/todos/{id}", (string id) =>
{
var todo = db.First(t => t.Id.ToString() == id);
db.Remove(todo);
});
The final feature will enable the addition of new items to the list. Navigate to the TodoItem.cs
file and append the following code:
record AddTodoItem(string? Name);
Next, in the Program.cs
file, add the following endpoint:
app.MapPost("/todos", (AddTodoItem command) =>
{
var todo = new TodoItem {Id = Guid.NewGuid(),Name = command.Name, IsCompleted = false };
db.Add(todo);
return new HtmlContentResult(Components.TodoItem(todo));
});
Navigate to the Components.cs
file and modify the Document
component to install the json-enc
extension to encode parameters in JSON format instead of URL format:
public static IHtmlContent Document(IHtmlContentContainer children)
{
var builder = new HtmlContentBuilder();
builder.AppendHtml("<!doctype html>");
builder.AppendHtml("<html>");
builder.AppendHtml("<head>");
builder.AppendHtml("<meta charset=\"UTF-8\">");
builder.AppendHtml("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">");
builder.AppendHtml("<script src=\"https://unpkg.com/htmx.org@1.9.6\" integrity=\"sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni\" crossorigin=\"anonymous\"></script>");
builder.AppendHtml("<script src=\"https://unpkg.com/htmx.org/dist/ext/json-enc.js\"></script>");
builder.AppendHtml("</head>");
builder.AppendHtml(children);
builder.AppendHtml("</html>");
return builder;
}
In the same file add a TodoForm
component:
public static IHtmlContent TodoForm()
{
var builder = new HtmlContentBuilder();
builder.AppendHtml("<form hx-post=\"/todos\" hx-swap=\"beforebegin\" hx-ext=\"json-enc\">");
builder.AppendHtml("<input type=\"text\" name=\"name\">");
builder.AppendHtml("<button type=\"submit\">Add</button>");
builder.AppendHtml("</form>");
return builder;
}
During the submit event, HTMX initiates a request (hx-post="/todos"
) in JSON format (hx-ext="json-enc"
) and appends the response in the parent element before the form (hx-swap="beforebegin"
). Finally, modify the TodoList
component to include the TodoForm
component:
public static IHtmlContent TodoList(IEnumerable<TodoItem> todoItems)
{
var builder = new HtmlContentBuilder();
builder.AppendHtml("<div>");
foreach (var todoItem in todoItems)
{
builder.AppendHtml(TodoItem(todoItem));
}
builder.AppendHtml(TodoForm());
builder.AppendHtml("</div>");
return builder;
}
In conclusion, HTMX is a powerful and lightweight JavaScript library that simplifies the process of building interactive web applications. By utilizing HTML attributes to make AJAX requests, HTMX offers an easy learning curve, improved performance, and seamless integration with server-side technologies. This beginner's guide provides a solid foundation for getting started with HTMX, showcasing its capabilities through a simple Todo App example. All the code is available here. Thanks, and happy coding.
Top comments (1)
Great post! I've tried the demo and it worked, now I understand what HTMX really is by doing. Thanks for that.
But I feel it's clunky to write HTML strings inside C# code, it will be much better if the demo replaces the HTML strings with razor files and uses
Razor.Templating.Core
package to render them into HTML strings.