February 02, 2021

Using HTMX with ASP.NET Core: Deleting items

Introduction

In the previous blog post, I gave a brief introduction to HTMX and demonstrated how you could add the HTMX script to an existing ASP.NET Core project. Now, let’s look at a practical example of using HTMX in an ASP.NET Core application.

In this blog post, I will demonstrate how to use HTMX to delete items from a list without doing a full-page postback. I will also explain an issue you may run into with anti-forgery tokens when using HTMX with Razor Pages, and give you some workarounds.

This series consists of the following blog posts:

  1. Introduction
  2. Deleting items from a list (this blog post)
  3. Do a page refresh when deleting items
  4. Inline editing (coming soon)
  5. Infinite scrolling (coming soon)
  6. Real-time search (coming soon)

The scenario

This first scenario is quite common; you have a list of items and want to allow users to delete items from the list. The typical approach to this is to do a postback when the user clicks on a delete button. The request handler will delete the item and then rerender the entire page.

We want to follow a similar approach, but instead of doing a postback, we want to perform an AJAX request that will delete the item from the database and then remove it from the list once the response of the AJAX request is received.

The demo application

I use an in-memory database with a list of customers created using Bogus. When the Razor page is loaded, I get the list of customers from the database and assign it to a Customers property that is accessed inside the Razor markup to render it as a list.

public class DeleteItemsModel : PageModel
{
    private readonly Database _database;

    public List<Customer> Customers { get; set; }

    public DeleteItemsModel(Database database)
    {
        _database = database;
    }

    public void OnGet()
    {
        Customers = _database.Customers;
    }
}

I have also created a Delete handler that will remove the customer with the specified Id from the database.

public IActionResult OnPostDelete(int id)
{
    _database.Customers.RemoveAll(customer => customer.Id == id);

    return new ContentResult();
}

For the Delete handler above, I would have preferred to return an HTTP Status 204 (i.e. a NoContentResult), but currently HTMX expects an HTTP Status 200 for it to know that the request was successful. You can follow this related issue to keep abreast of changes in this regard.

Below, you can see the abbreviated markup for the Razor page displaying the list of users. We have an unordered list (ul) with each customer’s information being displayed inside a list item (li). We also have a delete button inside each list item to extend with HTMX to perform the AJAX request.

<ul class="list-group list-group-lg list-group-flush list my--4">
    @foreach (var customer in Model.Customers)
    {
        <li>
            <div class="row align-items-center">
                <div class="col-auto">
                    <!-- User avatar goes here -->
                </div>
                <div class="col ml--2">
                    <!-- User name and email goes here -->
                </div>
                <div class="col-auto">
                    <!-- We will extend this button with HTMX to perform an AJAX request -->
                    <button>
                        Delete
                    </button>
                </div>
            </div>
        </li>
    }
</ul>

Adding HTMX to our list of items

When the user clicks on the “Delete” button, we want to perform an AJAX request to the Delete handler in our page model. To do that, we extend the HTML markup of the button by adding the hx-post attribute. The hx-post attribute informs HTMX that it should issue a POST to the URL specified as the attribute’s value.

As you can see below, we tell it to issue the POST to the current URL and pass the handler and id parameters to invoke the Delete handler with the id of the customer. We also specify the hx-swap attribute that tells HTMX to replace the Outer HTML of the target element with the response of the HTML request.

The default target is the element that initiated the request - in our case, that will be the button. We can override that by specifying the hx-target attribute as closest li. That tells HTMX to walk up the DOM from the button element and find its closest parent li ancestor.

<button hx-post="?handler=Delete&id=@customer.Id" 
        hx-target="closest li"
        hx-swap="outerHTML">
    Delete
</button>

Since the response from the AJAX request will be empty, what this effectively does is to remove the parent li from the DOM.

Dealing with anti-forgery tokens with HTMX AJAX requests

At this point, we can run the application and click on a button to delete a customer. Unfortunately, the AJAX request returns an HTTP status code 400.

HTTP Status 400 returned on POST

Upon further investigation, you can see in the log file that the anti-forgery token validation has failed.

info: Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.AutoValidateAntiforgeryTokenAuthorizationFilter[1]
      Antiforgery token validation failed. The required antiforgery request token was not provided in either form field "__RequestVerificationToken" or header value "RequestVerificationToken".
      Microsoft.AspNetCore.Antiforgery.AntiforgeryValidationException: The required antiforgery request token was not provided in either form field "__RequestVerificationToken" or header value "RequestVerificationToken".
         at Microsoft.AspNetCore.Antiforgery.DefaultAntiforgery.ValidateRequestAsync(HttpContext httpContext)
         at Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.ValidateAntiforgeryTokenAuthorizationFilter.OnAuthorizationAsync(AuthorizationFilterContext context)
info: Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker[3]
      Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.ViewFeatures.Filters.AutoValidateAntiforgeryTokenAuthorizationFilter'.

This anti-forgery token is used by ASP.NET Core to prevent cross-site request forgery (XSRF or CSRF) and is enabled by default in Razor Pages. I have discussed this issue and the potential workarounds in a previous blog post.

So how do we resolve the issue with the anti-forgery tokens when using HTMX?

There are a few different ways that you can handle this, namely:

  1. Appending a RequestVerificationToken header to the AJAX request
  2. Submitting a __RequestVerificationToken form field
  3. Use a form element and submit the contents of the form via AJAX

Appending a header to the AJAX request

HTMX has an event model that allows us to hook into certain lifecycle events. We can subscribe to the htmx:beforeRequest event which is triggered before an AJAX request is issued. We can access the XMLHttpRequest object from the detail.xhr property of the evt parameter and set the value of the “RequestVerificationToken” HTTP request header.

The code snippet below shows an example of the script to add to a Razor Page to enable this behaviour.

<!-- Add this to the top of your Razor Page -->
@using Microsoft.AspNetCore.Antiforgery
@inject IAntiforgery AntiForgery;

<!-- Then add this script somewhere else in the page -->
<script>
    document.body.addEventListener('htmx:beforeRequest', function(evt) {
        evt.detail.xhr.setRequestHeader("RequestVerificationToken", "@AntiForgery.GetAndStoreTokens(HttpContext).RequestToken");
    });
</script>

When we make an AJAX request, you can see the request is successful and the anti-forgery token is sent in the request headers:

Sending the anti-forgery token as a header

Submit and additional form field

HTMX has an hx-vars attribute that can be specified that allows you to send additional form data with POST. In this case, we can add a __RequestVerificationToken field with the value of the anti-forgery token.

<button hx-post="?handler=Delete&id=@customer.Id" 
        hx-target="closest li"
        hx-swap="outerHTML"
        hx-vars="__RequestVerificationToken:'@AntiForgery.GetAndStoreTokens(HttpContext).RequestToken'"
        class="btn btn-sm btn-danger d-none d-md-inline-block">
    Delete
</button>

When we make an AJAX request, you can see the anti-forgery token is sent as part of the form data:

Sending the anti-forgery token as form data

Submit a form via AJAX

The last possibility is to use a form and submit the form via AJAX. The reason that this works is that Razor Pages will automatically add a hidden field named __RequestVerificationToken to the form, ensuring that when the form is submitted, the anti-forgery token is passed in the form data.

To turn the form into an AJAX request, we can use the hx-boost attribute of HTMX which changes all anchors and form tags to use AJAX instead.

One important thing I have not mentioned thus far is that you don’t have to apply the HTMX hx-* attributes directly on the target element. You can also add the attributes to any of its parent elements in the DOM. With this in mind, we can change the markup of our list as follows:

<ul hx-boost="true"
    hx-target="closest li"
    hx-swap="outerHTML">
    @foreach (var customer in Model.Customers)
    {
        <li>
            <div class="row align-items-center">
                <div class="col-auto">
                    <!-- User avatar goes here -->
                </div>
                <div class="col ml--2">
                    <!-- User name and email goes here -->
                </div>
                <div class="col-auto">
                    <form asp-page-handler="Delete" asp-route-id="@customer.Id">
                        <button type="submit">
                            Delete
                        </button>
                    </form>
                </div>
            </div>
        </li>
    }
</ul>

Notice the following:

  1. I have placed the delete button inside a form element. The form is submitted when clicking the button.
  2. I have added an hx-boost element to the parent ul element, turning the form into an AJAX request. I also moved the other hx-* attributes to this element.

Personally, I prefer this approach since it allows me to leverage the built-in ASP.NET Core form tag helpers.

Confirm deletion

At the moment, users can delete a customer by clicking on the delete button. It would be great to have some confirmation to prevent accidental deletion of customer information.

HTMX allows you to specify the hx-confirm attribute with the confirmation message displayed to the user.

<ul hx-boost="true"
    hx-target="closest li"
    hx-swap="outerHTML"
    hx-confirm="Are you sure you want to delete this customer?">
    <!-- Some details omitted for brevity -->
</ul>

With this in place, once a user clicks on the delete button, they will be asked to confirm deletion of a customer. Under the hood, this uses the browser’s native confirm dialog box.

Displaying a confirmation dialog

Add animation

The final bit before finishing off this blog post is to add an animation when deleting a customer.

We can modify the amount of time that HTMX will wait after receiving a response to swap the content by including a swap modifier, for example, hx-swap="outerHTML swap:1s".

HTMX adds an htmx-swapping swapping class to an element that is about to be removed. Let’s specify a CSS class definition for the .htmx-swapping class with a transition to zero opacity:

.htmx-swapping {
  opacity: 0;
  transition: opacity 1s ease-out;
}

Now, when the user deletes an item, it will be slowly animated out before it is removed:

Animating deletion of a customer

Refer to the following HTMX docs for more information:

Conclusion

In this blog post, I demonstrated a common scenario where AJAX can be used in an ASP.NET Core application by allowing a user to delete an item from a list without doing a full-page postback.

I demonstrated how you could use HTMX to do this by adding a few simple attributes to your existing HTML, and also discussed potential workarounds for ASP.NET Core Razor Pages’ anti-forgery token verification.

You can find the source code for this series at https://github.com/jerriepelser-blog/htmx-with-aspnet-core.