Pagination

Pagination pattern with automatic route generation and state management

Pagination Style Options

The PaginationStyle enum defines different pagination display styles.


source

PaginationStyle


def PaginationStyle(
    args:VAR_POSITIONAL, kwds:VAR_KEYWORD
):

Display styles for pagination controls.

Pagination Class

The Pagination class manages paginated views with automatic route generation and state management. It follows the same pattern as StepFlow for consistent API design.

Key features: - Automatic pagination math (total pages, page ranges) - Auto-generated routes via create_router() - Query parameter preservation (filters, sorting, etc.) - Flexible data loading and rendering - HTMX integration for SPA-like navigation


source

Pagination


def Pagination(
    pagination_id:str, # Unique identifier for this pagination instance
    data_loader:Callable, # Function that returns all items
    render_items:Callable, # Function to render items for a page
    items_per_page:int=20, # Number of items per page
    container_id:str=None, # HTML ID for container (auto-generated if None)
    content_id:str=None, # HTML ID for content area (auto-generated if None)
    preserve_params:List=None, # Query parameters to preserve
    style:PaginationStyle=<PaginationStyle.SIMPLE: 'simple'>, # Pagination display style
    prev_text:str='« Previous', # Text for previous button
    next_text:str='Next »', # Text for next button
    page_info_format:str='Page {current} of {total}', # Format for page info
    button_size:str=None, # Button size class
    push_url:bool=True, # Whether to update URL with hx-push-url
    show_endpoints:bool=False, # Whether to show First/Last buttons
    first_text:str='«« First', # Text for first page button
    last_text:str='Last »»', # Text for last page button
    redirect_route:Optional=None, # Route to redirect non-HTMX requests
):

Manage paginated views with automatic route generation and state management.

Helper Methods


source

Pagination.get_total_pages


def get_total_pages(
    total_items:int, # Total number of items
)->int: # Total number of pages

Calculate total number of pages.


source

Pagination.get_page_items


def get_page_items(
    all_items:List, # All items
    page:int, # Current page number (1-indexed)
)->tuple: # (page_items, start_idx, end_idx)

Get items for the current page.


source

Pagination.build_route


def build_route(
    page:int, # Page number
    request:Any, # FastHTML request object
    page_route_func:Callable, # Route function from create_router
)->str: # Complete route with preserved params

Build route URL with preserved query parameters.

Rendering Methods


source

Pagination.render_navigation_controls


def render_navigation_controls(
    current_page:int, # Current page number
    total_pages:int, # Total number of pages
    route_func:Callable, # Function to generate route for page
)->FT: # Navigation controls element

Render pagination navigation controls.


source

Pagination.render_page_content


def render_page_content(
    page_items:List, # Items for current page
    current_page:int, # Current page number
    total_pages:int, # Total number of pages
    request:Any, # FastHTML request object
    route_func:Callable, # Function to generate route for page
)->FT: # Complete page content with items and navigation

Render complete page content with items and pagination controls.

Route Generation


source

Pagination.create_router


def create_router(
    prefix:str='', # URL prefix for routes (e.g., "/library")
)->APIRouter: # APIRouter with generated routes

Create FastHTML router with generated routes for pagination.

Usage Example

Here’s a complete example showing how to create a paginated view:

# Example: Simple paginated item list
from fasthtml.common import Div, H3, P, Li, Ul

# Define data loader function
def load_items(request):
    """Load all items - this could query a database, file system, etc."""
    # For demo, generate 100 items
    return [f"Item {i}" for i in range(1, 101)]

# Define render function for items
def render_items_list(items, page, request):
    """Render items for current page."""
    return Ul(
        *[Li(item) for item in items],
        cls="list-disc ml-6"
    )

# Create pagination instance
items_pagination = Pagination(
    pagination_id="items",
    data_loader=load_items,
    render_items=render_items_list,
    items_per_page=10
)

# Generate router
items_router = items_pagination.create_router(prefix="/items")

# In your FastHTML app, register the router:
# from cjm_fasthtml_app_core.core.routing import register_routes
# register_routes(app, items_router)
#
# Or directly:
# items_router.to_app(app)

# Access the pagination at: /items/content?page=1

# Test pagination math
print(f"Items per page: {items_pagination.items_per_page}")
print(f"Total pages for 100 items: {items_pagination.get_total_pages(100)}")
print(f"Total pages for 95 items: {items_pagination.get_total_pages(95)}")

# Test page items
all_items = load_items(None)
page1_items, start, end = items_pagination.get_page_items(all_items, 1)
print(f"\nPage 1: items {start+1}-{end} = {page1_items[:3]}...")

page5_items, start, end = items_pagination.get_page_items(all_items, 5)
print(f"Page 5: items {start+1}-{end} = {page5_items[:3]}...")
Items per page: 10
Total pages for 100 items: 10
Total pages for 95 items: 10

Page 1: items 1-10 = ['Item 1', 'Item 2', 'Item 3']...
Page 5: items 41-50 = ['Item 41', 'Item 42', 'Item 43']...
# Example 2: Pagination with preserved query parameters (filters, search, etc.)
from fasthtml.common import Div, H3, P

# Define data loader that uses query parameters
def load_media_files(request):
    """Load media files, filtered by query parameters."""
    # In real app, this would filter based on request.query_params
    media_type = request.query_params.get("media_type", "all") if request else "all"
    view = request.query_params.get("view", "grid") if request else "grid"
    
    # Simulate filtered results
    all_files = [f"file{i}.mp4" for i in range(1, 51)]
    
    # Return filtered files (simplified for demo)
    return all_files

# Define render function that uses query parameters
def render_media_grid(items, page, request):
    """Render media items in grid view."""
    media_type = request.query_params.get("media_type", "all") if request else "all"
    view = request.query_params.get("view", "grid") if request else "grid"
    
    return Div(
        H3(f"Media Library ({view} view, filter: {media_type})"),
        P(f"Showing {len(items)} files on page {page}"),
        Div(*[Div(item) for item in items])
    )

# Create pagination with preserved parameters
media_pagination = Pagination(
    pagination_id="media",
    data_loader=load_media_files,
    render_items=render_media_grid,
    items_per_page=20,
    preserve_params=["view", "media_type"],  # Automatically preserve these in URLs
    style=PaginationStyle.SIMPLE
)

# Generate router
media_router = media_pagination.create_router(prefix="/library")

# Now when users navigate pages, view and media_type are automatically preserved:
# /library/content?page=1&view=grid&media_type=video
# /library/content?page=2&view=grid&media_type=video  <- view and media_type preserved!

print("Media pagination configured with parameter preservation:")
print(f"  Preserved params: {media_pagination.preserve_params}")
print(f"  Items per page: {media_pagination.items_per_page}")
print(f"  Style: {media_pagination.style.value}")
Media pagination configured with parameter preservation:
  Preserved params: ['view', 'media_type']
  Items per page: 20
  Style: simple

Integration with FastHTML Applications

The Pagination class integrates seamlessly with FastHTML applications using the same pattern as StepFlow.

# This cell has been replaced by the new Pagination class above

Router Registration

After creating a Pagination instance and generating its router, register it with your FastHTML app:

from cjm_fasthtml_app_core.core.routing import register_routes

# Create pagination
library_pagination = Pagination(
    pagination_id="library",
    data_loader=load_library_items,
    render_items=render_library_grid,
    items_per_page=20,
    preserve_params=["view", "media_type"]
)

# Generate router
library_router = library_pagination.create_router(prefix="/library")

# Register with app
register_routes(app, library_router)

# Or directly:
# library_router.to_app(app)

The router automatically handles: - Pagination math (total pages, item slicing) - Query parameter preservation - Route generation with preserved state - HTMX integration

Data Loader and Render Functions

The Pagination class requires two key functions:

Data Loader Function

Loads all items based on request (can use query parameters for filtering):

def load_items(request):
    """Load all items - can filter based on query parameters."""
    # Access filters from query parameters
    media_type = request.query_params.get("media_type", "all")
    search = request.query_params.get("search", "")
    
    # Load and filter items
    items = get_all_items()
    
    if media_type != "all":
        items = [item for item in items if item.type == media_type]
    
    if search:
        items = [item for item in items if search.lower() in item.name.lower()]
    
    return items

Render Function

Renders the items for display (receives current page items, page number, and request):

def render_items(items, page, request):
    """Render items for current page."""
    # Can access query parameters if needed
    view_mode = request.query_params.get("view", "grid")
    
    if view_mode == "grid":
        return render_grid(items)
    else:
        return render_list(items)

The pagination automatically: - Calls data_loader to get all items - Slices items for current page - Passes page items to render_items - Adds navigation controls - Preserves specified query parameters

HTMX Integration

The Pagination class automatically configures HTMX attributes for SPA-like navigation:

  • hx-get: Fetches the new page content from the generated route
  • hx-target: Targets the content ID (auto-generated or custom)
  • hx-swap: Uses outerHTML to replace the entire content container
  • hx-push-url: Updates browser URL (configurable via push_url parameter)
  • href: Provides fallback for non-JavaScript navigation

Automatic Swap Strategy

The pagination uses outerHTML swap by default, which means: - The route returns a complete Div with id=content_id - HTMX replaces the entire element (including the ID) - This ensures the navigation controls are updated with each page change

Disabled States

Navigation buttons automatically handle boundary conditions: - Previous button disabled on page 1 (uses btn_behaviors.disabled) - Next button disabled on last page - Disabled buttons use DaisyUI styling

URL Management

When push_url=True (default): - Browser URL updates as users navigate pages - Users can bookmark specific pages - Back/forward buttons work correctly - Preserved query parameters are maintained in URLs

Example URL progression:

/library/content?page=1&view=grid&media_type=video
/library/content?page=2&view=grid&media_type=video  # page changed, other params preserved
/library/content?page=3&view=grid&media_type=video

Customization Options

You can customize the pagination behavior:

pagination = Pagination(
    pagination_id="results",
    data_loader=load_results,
    render_items=render_results_list,
    items_per_page=50,
    style=PaginationStyle.COMPACT,  # No page info display
    prev_text="← Previous",  # Custom button text
    next_text="Next →",
    button_size=str(btn_sizes.sm),  # Smaller buttons
    push_url=False  # Don't update URL (for modals, etc.)
)