f6ae86617c
Co-authored-by: Copilot <copilot@github.com>
156 lines
4.5 KiB
Markdown
156 lines
4.5 KiB
Markdown
# Pagination
|
||
|
||
A row of numbered page links — Previous, 1, 2, 3…, Next. Use it at the bottom of a list or table when there are too many items to show all at once. You give it the current page number, the total number of pages, and a URL pattern; it builds all the links automatically.
|
||
|
||
---
|
||
|
||
## Quick example
|
||
|
||
```csharp
|
||
new Pagination(current: 3, total: 10, urlPattern: "/blog?page={0}")
|
||
```
|
||
|
||
This renders a navigation row with links to pages 1–10 (with ellipsis for interior pages) and the Previous/Next arrows.
|
||
|
||
---
|
||
|
||
## All the options
|
||
|
||
```csharp
|
||
public Pagination(
|
||
int current,
|
||
int total,
|
||
string urlPattern)
|
||
```
|
||
|
||
| Parameter | What it does |
|
||
|---|---|
|
||
| `current` | The currently active page. 1-based (the first page is `1`). |
|
||
| `total` | The total number of pages. |
|
||
| `urlPattern` | A URL with `{0}` where the page number goes. E.g. `"/items?page={0}"`. |
|
||
|
||
The visible page window is at most 7 buttons. For large page counts, interior pages collapse into ellipsis (`…`) while the first page, last page, and pages close to `current` stay visible.
|
||
|
||
---
|
||
|
||
## Real-world examples
|
||
|
||
### Basic list with pagination
|
||
|
||
```html
|
||
<!-- Templates/BlogPage.htmx -->
|
||
<div class="space-y-6 mb-10">$$Posts$$</div>
|
||
$$Pager$$
|
||
```
|
||
|
||
```csharp
|
||
// Templates/BlogPage.htmx.cs
|
||
_pager = new Pagination(
|
||
current: page,
|
||
total: totalPages,
|
||
urlPattern: "/blog?page={0}");
|
||
```
|
||
|
||
### Preserving filters and sort order across pages
|
||
|
||
Build the URL pattern to include any query parameters that should survive page navigation:
|
||
|
||
```csharp
|
||
var urlPattern = $"/users?role={role}&sort={sort}&page={{0}}";
|
||
new Pagination(current: page, total: totalPages, urlPattern: urlPattern)
|
||
```
|
||
|
||
> Note the double braces `{{0}}` to produce a literal `{0}` after string interpolation.
|
||
|
||
### HTMX-powered pagination (no full page reload)
|
||
|
||
Wrap the pagination (and the content it controls) in a `hx-boost` container:
|
||
|
||
```html
|
||
<div hx-boost="true" hx-target="#item-list" hx-push-url="true">
|
||
<div id="item-list">$$Items$$</div>
|
||
$$Pager$$
|
||
</div>
|
||
```
|
||
|
||
---
|
||
|
||
## How it works
|
||
|
||
All links are plain `<a href="...">` elements — no JavaScript required. The URL for each page is built by calling `string.Format(urlPattern, pageNumber)`. When `current == 1`, the Previous link is styled as disabled (pointer-events removed, opacity reduced); same for Next when `current == total`.
|
||
|
||
---
|
||
|
||
## Complete page example
|
||
|
||
**`Templates/BlogPage.htmx`**
|
||
```html
|
||
<div class="max-w-3xl mx-auto py-10">
|
||
<h1 class="text-2xl font-bold mb-6">Blog</h1>
|
||
<div class="space-y-6 mb-10">
|
||
$$PostList$$
|
||
</div>
|
||
$$Pagination$$
|
||
</div>
|
||
```
|
||
|
||
**`Templates/BlogPage.htmx.cs`**
|
||
```csharp
|
||
namespace Htmx.ApiDemo.Templates;
|
||
|
||
public sealed class BlogPage : BlogPageBase
|
||
{
|
||
private readonly byte[] _postList;
|
||
private readonly IHtmxComponent _pagination;
|
||
|
||
public BlogPage(IEnumerable<BlogPost> posts, int currentPage, int totalPages)
|
||
{
|
||
var sb = new System.Text.StringBuilder();
|
||
foreach (var post in posts)
|
||
{
|
||
sb.Append($"""
|
||
<article class="border-b pb-6">
|
||
<h2 class="text-lg font-semibold mb-1">
|
||
<a href="/blog/{System.Net.WebUtility.HtmlEncode(post.Slug)}"
|
||
class="hover:underline">
|
||
{System.Net.WebUtility.HtmlEncode(post.Title)}
|
||
</a>
|
||
</h2>
|
||
<p class="text-sm text-muted-foreground mb-2">{post.PublishedAt:MMMM d, yyyy}</p>
|
||
<p class="text-sm">{System.Net.WebUtility.HtmlEncode(post.Summary)}</p>
|
||
</article>
|
||
""");
|
||
}
|
||
_postList = sb.ToString().ToUtf8Bytes();
|
||
_pagination = new Components.Pagination(
|
||
current: currentPage,
|
||
total: totalPages,
|
||
urlPattern: "/blog?page={0}");
|
||
}
|
||
|
||
protected override void RenderPostList(HtmxRenderContext ctx) => ctx.Writer.WriteUtf8(_postList);
|
||
protected override void RenderPagination(HtmxRenderContext ctx) => _pagination.Render(ctx.Next());
|
||
}
|
||
```
|
||
|
||
**GET handler**
|
||
```csharp
|
||
[Handler]
|
||
[MapGet("/blog")]
|
||
public static partial class GetBlogHandler
|
||
{
|
||
public record Query([property: FromQuery] int Page = 1);
|
||
|
||
private static async Task<IResult> HandleAsync(
|
||
Query q, HttpContext ctx, BlogService blog, CancellationToken ct)
|
||
{
|
||
const int pageSize = 10;
|
||
var (posts, total) = await blog.GetPageAsync(q.Page, pageSize, ct);
|
||
int totalPages = (int)Math.Ceiling(total / (double)pageSize);
|
||
|
||
return await ctx.WriteHtmxPage(
|
||
new BlogPage(posts, q.Page, totalPages), title: "Blog");
|
||
}
|
||
}
|
||
```
|