Getting Started with HTMX

By FoxLearn 2/7/2025 8:09:23 AM   52
HTMX is a JavaScript library that lets you integrate AJAX functionality into HTML directly through HTML attributes.

It operates on two main concepts:

  1. Enabling any HTML element to trigger an HTTP request.
  2. 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:

  1. 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.

  2. POST /greet: This endpoint responds with a <div> displaying the current UTC date and time.

To see the example in action:

  1. Run the application.
  2. 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 the hx-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.