February 09, 2021

Using HTMX with ASP.NET Core: Deleting items with a page refresh

Introduction

In the previous blog post, I demonstrated how you could delete items with AJAX using HTMX and ASP.NET Core. In this blog post, I further demonstrate how you can implement a page refresh when deleting items.

Displaying an empty state

In this series so far, we have built an application that allows users to delete items from a list using AJAX. After deleting an item from the back-end, we remove it from the list of items on the web page. We have the problem that, once all items are removed from the list, we end up with an empty grid.

An empty grid once all customers are deleted

It is a common UX pattern to provide an empty state that is more useful to the user. For example, instead of displaying an empty grid, we can show an informative message to the user and give them easy access to adding new items to the list.

Empty state allowing users to add a new item

To achieve this, we can update the existing customer list page with simple if-then-else logic.

@if (Model.Customers.Any())
{
    <ul hx-boost="true"
        hx-target="closest li"
        hx-swap="outerHTML swap:1s"
        hx-confirm="Are you sure you want to delete this customer?">
        @foreach (var customer in Model.Customers)
        {
            <!-- Display list of customers -->
        }
    </ul>
}
else
{
    <!-- Display empty state -->
}

This will work fine if we do a full-page postback and rerender the Razor page every time, but that’s not what we are doing. Instead, we are deleting a customer via an AJAX request and not refreshing the page or any part of the page when the AJAX request completes.

There are a couple of ways we can resolve this. The first way is to force a full page refresh when there are no more customers to display the empty state to the user. The second way is to refresh the section of the page that displays this list of customers or the empty state in the case where there are no customers.

Performing a full page refresh

Forcing a full page refresh with HTMX is a simple process. HTML has a set of predefined headers that we can use to communicate state from the back-end of the application to the front-end. By setting the HX-Refresh header of the response to “true”, we can tell HTMX that it should perform a full refresh of the current page.

Let’s update the Delete handler to check whether we have any remaining customers after deleting the requested customer. If we don’t, we set the HX-Refresh header to “true” to tell HTMX to refresh the entire page.

Refreshing the page will rerender the Razor page, ensuring that we will execute the if-then-else logic and display the empty state to the user.

public class DeleteWithRefreshModel : PageModel
{
    // Some code omitted for brevity
    
    public IActionResult OnPostDelete(int id)
    {
        _database.Customers.RemoveAll(customer => customer.Id == id);

        if (_database.Customers.Count == 0)
        {
            Response.Headers.Add("HX-Refresh", "true");
        }

        return new OkResult();
    }
}

You can see the user experience for this in the animated screenshot below. If you observe carefully, you can see that after the final customer is deleted, the browser does a full page refresh.

Deleting all customers and refreshing the page

When viewing the network traffic in the browser’s developer tools, you can see that we have 2 AJAX POST requests to delete the last two customers. After that, we have a standard GET request for the refreshing of the page.

Requests in the browser developer tools

If I select the last of the two AJAX requests, you can see the HX-Refresh header in the response. This header instructs HTMX to perform the subsequent GET request to perform the full page refresh.

HX-Refresh response header in the last AJAX request

Performing a partial page refresh

In the scenario above, we only refreshed the page when all items have been removed from the list. There are other situations where you may want to refresh the page every time an item is removed.

Let’s say, for example, that we are displaying a paged list of items with 20 items on a page. When you delete an item from the list, the number of items will reduce to 19 items. Since we want to display 20 items on the page at all times, we will need to refresh the entire list.

We can force a page refresh each time an item is deleted, but that does not make sense as then there is no need to make AJAX calls. In situations like this, it is best to do a partial page refresh and only update the section of the page that contains the list of items.

To do this, let’s first extract the logic that displays the list of customers and empty state into a partial named _CustomerList.cshtml. Note that we remove the hx-* attributes from the ul as we will be pulling them up to the element containing the partial.

<!-- _CustomerList.cshtml -->

@model IEnumerable<Customer>

@if (Model.Any())
{
    <ul>
        @foreach (var customer in Model)
        {
            <!-- Display list of customers -->
        }
    </ul>
}
else
{
    <!-- Display empty state -->
}

Next, we can update our page to place the partial inside a containing div element, and place the hx-* attributes on this div element.

Note that we have updated the hx-target to this. This means that swap will target this div. We also changed the hx-swap to innerHTML. This tells HTMX that we want to replace the innerHTML of the div when doing the swap.

<div hx-boost="true"
        hx-target="this"
        hx-swap="innerHTML"
        hx-confirm="Are you sure you want to delete this customer?">
    
    <!-- We will rerender and replace the contents of this partial every time 
         an item is deleted -->
    <partial name="_CustomerList" model="Model.Customers"/>

</div>

Finally, we can change the Delete handler to render the partial view each time an item is deleted from the list and sending the rendered content to the browser. Since the logic for displaying an empty state is contained inside the partial, we can be assured that the empty state will be displayed correctly if all items are deleted from the list.

public class DeleteWithPartialRefreshModel : PageModel
{
    // Some code omitted for brevity
    
    public IActionResult OnPostDelete(int id)
    {
        _database.Customers.RemoveAll(customer => customer.Id == id);

        return Partial("_CustomerList", _database.Customers);
    }
}

When running the application and deleting an item from the list, you can see that, same as before, we are making AJAX request to the back-end. However, if we inspect the AJAX request in the browser’s developer tools, you can see that the response contains the rendered partial. HTMX will take this response and replace the existing content of the div containing the partial with the newly rendered content.

The rendered partial in the AJAX response

When to use each technique

You may be asking yourself when to use each of these techniques. The correct answer is that it depends on you, but my recommendations would be as follows.

It is straightforward to add the HX-Refresh header to existing code, so this should probably your preferred choice. This approach works well in cases where you have a single page that lists all the items in a list, and you can simply remove items from the list when they are deleted.

There are, however, other scenarios where you may want to refresh the entire list such as when you display a paged list of items. In scenarios like these, I would recommend going for the second solution and do a partial refresh of the list every time.

Conclusion

There are situations where you may want to refresh the page each time an item is deleted from a list. In this blog post, I demonstrated a couple of ways to do this using HTMX and ASP.NET Core.

More of this series

This blog post is part of a series on using HTMX with ASP.NET Core. The full source code for the series can be found at https://github.com/jerriepelser-blog/htmx-with-aspnet-core.

These are all the blog posts in the series:

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