Getting Started with HTMX
By FoxLearn 2/7/2025 8:09:23 AM 52
It operates on two main concepts:
- Enabling any HTML element to trigger an HTTP request.
- Updating the element with the response HTML.
Here’s a simple example:
<div hx-get="/items" hx-trigger="load" hx-swap="innerHTML"> Loading items... </div>
In this case, the <div>
will:
- Automatically make an HTTP GET request to
/items
when the page loads. - Replace the content inside the
<div>
with the returned HTML from the response.
Why consider HTMX when there are so many front-end frameworks available today?
Here are a few important reasons:
Simplicity: HTMX lets developers create interactive web apps with simple HTML attributes, reducing the need for complex JavaScript code.
Performance: By relying primarily on HTML and minimal JavaScript, HTMX often results in faster page loads and reduced memory usage.
Learning Curve: Since HTMX integrates directly with HTML, it has a much lower learning curve for developers already familiar with HTML/CSS.
Server-Side Rendering: HTMX is compatible with any server-side technology, enabling developers to work in their preferred backend languages.
Hello World Example
The classic "Hello World" is always a great way to get started with new technologies. Here's how you can set up a similar example with HTMX:
Start by running the following commands to create the project and solution:
dotnet new web -o MyApi dotnet new sln -n HtmxExample dotnet sln add --in-root MyApi
In the MyApi
project, add a new file called HtmlResult.cs
with the following content:
using System.Net.Mime; using System.Text; namespace MyApi; 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 allow us to return HTML from the API endpoints. Now, update the Program.cs
file with the following code:
using Microsoft.AspNetCore.Html; using MyApi; var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapGet("/greet", () => 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/[email protected]"" integrity=""sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni"" crossorigin=""anonymous""></script> </head> <body> <button hx-post=""/greet"" hx-swap=""outerHTML"">Say Hello</button> </body> </html> ")); app.MapPost("/greet", () => new HtmlResult($@"<div>Hello, the time is {DateTimeOffset.UtcNow}</div>")); app.Run();
In this example, there are two endpoints:
GET
/greet
: This returns an HTML page that includes the HTMX library and a button. When the button is clicked, an HTTP POST request is sent to/greet
, and the button is replaced with the response.POST
/greet
: This endpoint responds with a<div>
displaying the current UTC date and time.
To see the example in action:
- Run the application.
- Open your browser and go to http://localhost:5179/greet.
When you click the Say Hello button, it will be replaced with the current time, demonstrating how HTMX can update content dynamically without requiring full page reloads.
Attributes
HTMX provides several key attributes designed for making AJAX requests:
- hx-get: Sends a GET request to the specified URL.
- hx-post: Sends a POST request to the specified URL.
- hx-put: Sends a PUT request to the specified URL.
- hx-patch: Sends a PATCH request to the specified URL.
- hx-delete: Sends a DELETE request to the specified URL.
hx-trigger
The hx-trigger
attribute defines what will trigger an AJAX request.
By default, you don’t need to specify it for the following elements:
- input, textarea, and select are triggered by the
change
event. - form is triggered by the
submit
event. - All other elements are triggered by the
click
event.
The value of hx-trigger
can be one of these:
- An event name.
- A polling definition.
- A comma-separated list of events.
hx-target
The hx-target
attribute allows you to specify a different element to update (swap content) instead of the one that triggered the AJAX request. The value of this attribute can be:
- A CSS selector to target a specific element.
this
, which indicates that the element with thehx-target
attribute is the target itself.closest <CSS selector>
, which targets the nearest ancestor element (or the element itself) that matches the given CSS selector.find <CSS selector>
, which targets the first child or descendant element that matches the given CSS selector.next <CSS selector>
, which looks for the next element in the DOM that matches the given CSS selector.previous <CSS selector>
, which looks for the previous element in the DOM that matches the given CSS selector.
hx-swap
The hx-swap
attribute defines how the response will be inserted relative to the target element after an AJAX request. The available options are:
- innerHTML: Inserts the response content inside the target element (default option).
- outerHTML: Replaces the entire target element with the response.
- beforebegin: Inserts the response before the target element within the parent.
- afterbegin: Inserts the response before the first child of the target element.
- beforeend: Inserts the response after the last child of the target element.
- afterend: Inserts the response after the target element within the parent.
- delete: Removes the target element, ignoring the response.
- none: Does not insert any content from the response.
Dynamic Blog Post Example with HTMX and .NET
Let’s build a simple dynamic blog post application using HTMX with .NET. We'll implement functionality such as fetching blog posts, adding new posts, and liking a post, all without page reloads.
Instead of manually creating HTML strings, we'll use HtmlContentBuilder
to generate the necessary HTML. This will help us build dynamic, interactive web applications efficiently.
Step 1: Create the HtmlContentResult
We begin by creating a custom result type, HtmlContentResult
, that implements IResult
and returns dynamically generated HTML content:
using Microsoft.AspNetCore.Html; using System.Net.Mime; using System.Text.Encodings.Web; using System.Text; namespace BlogApi; 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); } } }
Step 2: Create the BlogPost Model
Next, define a simple BlogPost
model to represent each blog post:
namespace BlogApi; public class BlogPost { public Guid Id { get; set; } public string Title { get; set; } public string Content { get; set; } public int Likes { get; set; } }
Step 3: Define HTML Builders in Components.cs
We'll now create Components.cs
to hold the HTML structure generation logic. This will include methods to render blog posts and the form to add a new post.
using Microsoft.AspNetCore.Html; namespace BlogApi; 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/[email protected]\" integrity=\"sha384-FhXw7b6AlE/jyjlZH5iHa/tTe9EpJ1Y55RjcgPbjeWMskSxZt1v9qkxLJWNJaGni\" crossorigin=\"anonymous\"></script>"); builder.AppendHtml("</head>"); builder.AppendHtml(children); builder.AppendHtml("</html>"); return builder; } public static IHtmlContent BlogPostList(IEnumerable<BlogPost> blogPosts) { var builder = new HtmlContentBuilder(); builder.AppendHtml("<div>"); foreach (var post in blogPosts) { builder.AppendHtml(BlogPost(post)); } builder.AppendHtml("</div>"); return builder; } public static IHtmlContent BlogPost(BlogPost post) { var builder = new HtmlContentBuilder(); builder.AppendHtml("<div>"); builder.AppendHtml("<h2>"); builder.AppendFormat("{0}", post.Title); builder.AppendHtml("</h2>"); builder.AppendHtml("<p>"); builder.AppendFormat("{0}", post.Content); builder.AppendHtml("</p>"); builder.AppendHtml("<button hx-post=\"/like/{post.Id}\" hx-target=\"closest div\" hx-swap=\"outerHTML\">Like ({post.Likes})</button>"); builder.AppendHtml("</div>"); return builder; } public static IHtmlContent AddPostForm() { var builder = new HtmlContentBuilder(); builder.AppendHtml("<form hx-post=\"/posts\" hx-swap=\"beforebegin\">"); builder.AppendHtml("<input type=\"text\" name=\"title\" placeholder=\"Title\">"); builder.AppendHtml("<textarea name=\"content\" placeholder=\"Content\"></textarea>"); builder.AppendHtml("<button type=\"submit\">Add Post</button>"); builder.AppendHtml("</form>"); return builder; } }
Step 4: Set Up Endpoints in Program.cs
Now let's set up the Program.cs
file with endpoints to handle rendering the blog post list, adding new posts, and liking a post.
var db = new List<BlogPost>() { new BlogPost() { Id = Guid.NewGuid(), Title = "Introduction to HTMX", Content = "This is a beginner guide to HTMX.", Likes = 5 }, new BlogPost() { Id = Guid.NewGuid(), Title = "Getting Started with .NET", Content = "Learn how to build web apps with .NET.", Likes = 3 }, }; app.MapGet("/", () => { var builder = new HtmlContentBuilder(); builder.AppendHtml("<body hx-get=\"/posts\" hx-trigger=\"load\" hx-swap=\"innerHTML\">"); builder.AppendHtml("</body>"); return new HtmlContentResult(Components.Document(builder)); }); app.MapGet("/posts", () => new HtmlContentResult(Components.BlogPostList(db)));
Step 5: Implement the "Like" Button
Add functionality for liking a blog post. When the like button is clicked, HTMX will send a POST request, and the post will be updated to reflect the new like count.
app.MapPost("/like/{id}", (string id) => { var post = db.First(p => p.Id.ToString() == id); post.Likes++; return new HtmlContentResult(Components.BlogPost(post)); });
Step 6: Implement Adding New Posts
To allow users to add new blog posts, we'll create an endpoint to handle the form submission and update the list of posts.
First, create a record for the AddBlogPost
command:
record AddBlogPost(string Title, string Content);
Then, add the following endpoint:
app.MapPost("/posts", (AddBlogPost command) => { var newPost = new BlogPost { Id = Guid.NewGuid(), Title = command.Title, Content = command.Content, Likes = 0 }; db.Add(newPost); return new HtmlContentResult(Components.BlogPost(newPost)); });
Step 7: Update the Main Page Layout
We now need to update the main page layout to include the post submission form, so users can add new posts.
Modify the Components.cs
file to include the post submission form in the list of posts:
public static IHtmlContent BlogPostList(IEnumerable<BlogPost> blogPosts) { var builder = new HtmlContentBuilder(); builder.AppendHtml("<div>"); foreach (var post in blogPosts) { builder.AppendHtml(BlogPost(post)); } builder.AppendHtml(AddPostForm()); builder.AppendHtml("</div>"); return builder; }
Here’s how the HTMX-powered blog post application works:
- Loading Posts: When the page loads, HTMX fetches the list of posts via
hx-get="/posts"
and renders them dynamically. - Adding Posts: A form allows users to add new blog posts without refreshing the page. The new post appears above the form after submission.
- Liking Posts: Users can like posts by clicking the like button. This triggers an AJAX POST request, and the like count is updated in the UI without a page reload.
HTMX offers a simple, yet powerful way to enhance the interactivity of your application using HTML attributes, and combining it with server-side technologies like .NET makes it a great choice for building dynamic web apps.