Files
Htmx/docs/Components/Pagination.md
T
2026-05-04 19:57:48 +05:00

173 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Pagination
A page navigation row with Prev/Next links and numbered page buttons. The current page is highlighted. Links are built from a URL pattern.
---
## HTML structure
```
nav[aria-label=Pagination].flex.items-center.justify-center.gap-1
a.pag-btn[href=prevUrl, aria-label=Previous] ← disabled styling when current=1
svg (chevron-left)
a.pag-btn[href=url] ← one per page in the visible window
{pageNumber} ← current page has pag-btn-active class
span.pag-ellipsis ← rendered when pages are skipped
a.pag-btn[href=nextUrl, aria-label=Next] ← disabled styling when current=total
svg (chevron-right)
```
---
## CSS mechanics
| Class | Effect |
|---|---|
| `pag-btn` | `inline-flex h-9 w-9 items-center justify-center rounded-md border border-input bg-background text-sm hover:bg-accent` |
| `pag-btn-active` | `bg-primary text-primary-foreground border-primary hover:bg-primary/90` |
| `pag-ellipsis` | `inline-flex h-9 w-9 items-center justify-center text-sm text-muted-foreground` |
| `pointer-events-none opacity-50` | Applied to Prev when `current == 1`, to Next when `current == total` |
The visible page window is limited to 7 buttons maximum. For large page counts the component collapses interior pages into ellipsis spans, keeping first page, last page, and the pages immediately around `current` always visible.
---
## Constructor signature
```csharp
public Pagination(
int current,
int total,
string urlPattern)
```
| Parameter | Description |
|---|---|
| `current` | 1-based current page number |
| `total` | Total number of pages |
| `urlPattern` | URL template with `{0}` replaced by the page number, e.g. `"/items?page={0}"` |
---
## Usage examples
### Basic pagination
```csharp
new Pagination(current: 3, total: 10, urlPattern: "/blog?page={0}")
```
### Preserving query parameters
```csharp
// Build the URL pattern from the current request
var query = HttpUtility.ParseQueryString(Request.QueryString.ToString());
query["page"] = "{0}";
var pattern = "/search?" + query.ToString();
new Pagination(current: page, total: totalPages, urlPattern: pattern)
```
### HTMX-powered pagination (swap content without full navigation)
The links are standard `<a>` tags. To intercept them with HTMX, use `hx-boost` on the container or wrap in a boosted `<div>`:
```html
<div hx-boost="true" hx-target="#item-list" hx-push-url="true">
$$Pagination$$
</div>
```
### Single page (hides naturally)
```csharp
// When total == 1, Prev and Next are both disabled and only "1" is rendered.
new Pagination(current: 1, total: 1, urlPattern: "/items?page={0}")
```
---
## Tips and tricks
- The `urlPattern` uses `string.Format`-style `{0}` — do not use `{page}` or other named placeholders.
- Page numbers are 1-based throughout — the first page is page `1`.
- When `total` is 0 or negative the component renders nothing — guard `total > 1` in the page if you want to hide it entirely when there is only one page.
- To preserve sort order or filters across pages, include those values in the `urlPattern` query string.
- For very small page counts (23 pages), all page buttons are shown with no ellipsis.
- For very small page counts (23 pages), all page buttons are shown with no ellipsis.
---
## 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");
}
}
```