Adding Paging with HTMX and ASP.NET Core with TagHelper (English)

Adding Paging with HTMX and ASP.NET Core with TagHelper

Friday, 09 August 2024

//

3 minute read

Introduction

Now that I have a bunch of blog posts the home page was getting rather length so I decided to add a paging mechanism for blog posts.

This goes along with adding full caching for blog posts to make this a quick and efficient site.

See the Blog Service source for how this is implemented; it's really pretty simple using the IMemoryCache.

TagHelper

I decided to use a TagHelper to implement the paging mechanism. This is a great way to encapsulate the paging logic and make it reusable. This uses the pagination taghelper from Darrel O'Neill this is included in the project as a nuget package.

This is then added to the _ViewImports.cshtml file so it is available to all views.

@addTagHelper *,PaginationTagHelper.AspNetCore

The TagHelper

In the _BlogSummaryList.cshtml partial view I added the following code to render the paging mechanism.

<pager link-url="@Model.LinkUrl"
       hx-boost="true"
       hx-push-url="true"
       hx-target="#content"
       hx-swap="show:none"
       page="@Model.Page"
       page-size="@Model.PageSize"
       total-items="@Model.TotalItems" ></pager>

A few notable things here:

  1. link-url this allows the taghelper to generate the correct url for the paging links. In the HomeController Index method this is set to that action.
   var posts = blogService.GetPostsForFiles(page, pageSize);
   posts.LinkUrl= Url.Action("Index", "Home");
   if (Request.IsHtmx())
   {
      return PartialView("_BlogSummaryList", posts);
   }

And in the Blog controller

    public IActionResult Index(int page = 1, int pageSize = 5)
    {
        var posts = blogService.GetPostsForFiles(page, pageSize);
        if(Request.IsHtmx())
        {
            return PartialView("_BlogSummaryList", posts);
        }
        posts.LinkUrl = Url.Action("Index", "Blog");
        return View("Index", posts);
    }

This is set to that URl. This ensures the pagination helper can work for either top level method.

HTMX Properties

hx-boost, hx-push-url, hx-target, hx-swap these are all HTMX properties that allow the paging to work with HTMX.

     hx-boost="true"
       hx-push-url="true"
       hx-target="#content"
       hx-swap="show:none"

Here we use hx-boost="true" this allows the pagination taghelper to not need any modifications by intercepting it's normal URL generation and using the current URL.

hx-push-url="true" to ensure the URL is swapped in the browser's URL history (which allows direct linking to pages).

hx-target="#content" this is the target div that will be replaced with the new content.

hx-swap="show:none" this is the swap effect that will be used when the content is replaced. In this case it prevents the normal 'jump' effect which HTMX uses on swapping content.

CSS

I also added styles to the main.css in my /src directory allowing me to use the Tailwind CSS classes for the pagination links.

.pagination {
    @apply py-2 flex list-none p-0 m-0 justify-center items-center;
}

.page-item {
    @apply mx-1 text-black  dark:text-white rounded;
}

.page-item a {
    @apply block rounded-md transition duration-300 ease-in-out;
}

.page-item a:hover {
    @apply bg-blue-dark text-white;
}

.page-item.disabled a {
    @apply text-blue-dark pointer-events-none cursor-not-allowed;
}

Controller

page, page-size, total-items are the properties that the pagination taghelper uses to generate the paging links. These are passed into the partial view from the controller.

 public IActionResult Index(int page = 1, int pageSize = 5)

Blog Service

Here page and pageSize are passed in from the URL and the total items are calculated from the blog service.

    public PostListViewModel GetPostsForFiles(int page=1, int pageSize=10)
    {
        var model = new PostListViewModel();
        var posts = GetPageCache().Values.Select(GetListModel).ToList();
        model.Posts = posts.OrderByDescending(x => x.PublishedDate).Skip((page - 1) * pageSize).Take(pageSize).ToList();
        model.TotalItems = posts.Count();
        model.PageSize = pageSize;
        model.Page = page;
        return model;
    }

Here we simply get the posts from the cache, order them by date and then skip and take the correct number of posts for the page.

Conclusion

This was a simple addition to the site but it makes it much more usable. The HTMX integration makes the site feel more responsive while not adding more JavaScript to the site.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.