June 03, 2021

Using HTMX with ASP.NET Core: Infinite scrolling

Introduction

Continuing with my series on using HTMX with ASP.NET Core, I want to demonstrate how you can implement infinite scrolling. For this example, we are working with an application that displays a list of tweets.

List of tweets

The Razor markup that displays this list is pretty straightforward. I loop through a list of tweets and, for each tweet, I show the username and avatar. I also display the content of the tweet and the time it was posted.

<h1 class="mb-5">
    Tweets
</h1>
@foreach (var tweet in Model.InitialTweets)
{
    <div class="media mb-2">
        <img src="@tweet.Avatar" class="mr-3" alt="Avatar for @tweet.Username" style="width: 50px; height: 50px;">
        <div class="media-body">
            <p class="mb-1"><strong>@tweet.Username</strong></p>
            <p class="mb-1">@tweet.Text</p>
            <p class="small text-muted">@tweet.Time.ToString("f")</p>
        </div>
    </div>
}

Since the list of tweets can run into the thousands, I don’t want to display all of them at once. When the page loads, I want to display the 20 latest tweets. When the user scrolls past the last tweet, I want to make an AJAX call to load the next 20 tweets.

Loading the initial set of tweets

When the page loads, I grab the 20 latest tweets and assign that to an InitialTweets list.

public class InfiniteScrolling : PageModel
{
    public List<Tweet> InitialTweets { get; set; }

    public void OnGet()
    {
        InitialTweets = _database.Tweets.OrderByDescending(t => t.Time).Take(20).ToList();
    }
}

Since I will be rendering a list of tweets on the initial page render and on each of the AJAX calls that loads the next set of 20 tweets, I extract the rendering of the tweets to a new partial view called _TweetList.

@model IEnumerable<Tweet>

@foreach (var tweet in Model)
{
    <div class="media mb-2">
        <img src="@tweet.Avatar" class="mr-3" alt="Avatar for @tweet.Username" style="width: 50px; height: 50px;">
        <div class="media-body">
            <p class="mb-1">
                <strong>@tweet.Username</strong>
            </p>
            <p class="mb-1">@tweet.Text</p>
            <p class="small text-muted">@tweet.Time.ToString("f")</p>
        </div>
    </div>
}

And update my Razor page markup to call the partial view, passing the InitialTweets and the model for the partial view.

<h1 class="mb-5">
    Tweets
</h1>
<partial name="_TweetList" model="@Model.InitialTweets"/>

Loading the next set of tweets

For loading the next set of tweets, I am using three different HTMX attributes:

  1. I use the hx-get attribute to issue an AJAX GET request to the URL I specify.
  2. I use the hx-trigger attribute to tell HTMX which event should trigger the AJAX request.
  3. I use the hx-swap attribute to tell HTMX what it should do with the response from the AJAX request.

Putting this all together, I add the following div after my list of tweets:

@foreach (var tweet in Model)
{
    <!-- Some code omitted -->
}
<div 
    hx-get="@Url.Page("", "LoadTweets", new {after = Model.Last().Time.ToString("O")})" 
    hx-trigger="revealed" 
    hx-swap="outerHTML"></div>

This tells HTMX that when that div is revealed (i.e. it becomes visible on the page), it should make an AJAX GET request to the specified URL - in this case, a LoadTweets handler on my with the date and time of the last tweet as the after parameter.

The hx-swap attribute tells HTMX that once the AJAX request returns a response, it should take that response and replace the entire div with the response.

For the LoadTweets page handler, I grab the subsequent 20 tweets after the date and time specified in the after parameter and render the _TweetList partial, passing the tweets as its model.

public IActionResult OnGetLoadTweets(DateTime after)
{
    var next20Tweets = _database.Tweets
        .OrderByDescending(t => t.Time)
        .Where(t => t.Time < after)
        .Take(20)
        .ToList();

    return Partial("_TweetList", next20Tweets);
}

With this in place, let’s try out the application. If you keep an eye on the browser’s scroll bar, you will notice that the next set of tweets are loaded once I get to the end of the page. In the screenshot, I also display the network traffic so you can see the HTTP requests.

Scrolling through the list of tweets

Reaching the end of the list

You will notice in the demo above that the last AJAX request returns an HTTP status code of 500. There is an error in the application’s logic where I try and do a Last() on the list of tweets, but there are no items in the list. Let’s fix this error and handle the situation when there are no more available tweets more elegantly.

I update the _TweetList partial to check whether there are items in the list of tweets and conditionally render either the list of tweets or a message indicating no more tweets.

@if (Model.Any())
{
    @foreach (var tweet in Model)
    {
        <!-- Some code omitted -->
    }
    <div
        hx-get="@Url.Page("", "LoadTweets", new {after = Model.Last().Time.ToString("O")})"
        hx-trigger="revealed"
        hx-swap="outerHTML">
    </div>
}
else
{
    <p class="text-center">No more tweets 😭</p>
}

Reaching the end of the list

Displaying a loading indicator

In my sample application, the next set of tweets load almost instantaneous but, in a real-world scenario, it will likely take some time to load the next set of records from your data source.

From a user experience point of view, it will be useful to indicate to the user that we are loading more items. For this, we can use hx-indicator attribute.

As the value of the hx-indicator attribute, you specify the id of an HTML element on your page. While an AJAX request is executing, HTMX will add the htmx-request class to that element. Let’s add the following CSS to our application:

#loading-indicator {
  display:none;
}

#loading-indicator.htmx-request {
  display:inline;
}

This CSS will hide the loading-indicator element by default, but it will be displayed when adding the htmx-request class.

On the Razor page, we can add a new div with an id of loading-indicator and then, on the outer div that contains this div as well as the tweets partial, we add the hx-indicator with a value of #loading-indicator.

<div class="col" hx-indicator="#loading-indicator">
    <h1 class="mb-5">
        Tweets
    </h1>
    <partial name="_TweetList" model="@Model.InitialTweets"/>
    <div id="loading-indicator" class="m-3">Loading more tweets...</div>
</div>

In the demo below, I simulate a 2-second delay when fetching new tweets. You can see the “Loading more tweets…” displayed to the user while fetching the next set of tweets.

Conclusion

In this blog post, I demonstrated how you could use HTMX with ASP.NET Core to implement infinite scrolling on a long list of items. The beauty of this approach we are developing a regular Razor Page application and simply adding progressive enhancements to the page by making use of various HTMX attributes.

More of this series

This blog post is part of a series on using HTMX with ASP.NET Core. You can find the complete source code for the series 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
  4. Inline editing
  5. Infinite scrolling (this blog post)
  6. Real-time search (coming soon)