Skip to content

Pagination

Display large datasets across multiple pages with automatic URL synchronization and reactive navigation.

Overview

Pagination in Diffyne is as easy as Livewire. Use the HasPagination trait to add pagination functionality to any component. It automatically handles:

  • Page navigation - Next, previous, and direct page links
  • URL synchronization - Page number in URL query string
  • State management - Automatic serialization of paginator objects
  • Reactive updates - UI updates automatically when page changes

Quick Start

1. Use the Trait

php
<?php

namespace App\Diffyne;

use Diffyne\Component;
use Diffyne\Traits\HasPagination;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

class UserList extends Component
{
    use HasPagination;

    public ?LengthAwarePaginator $users = null;

    public function mount(): void
    {
        $this->loadUsers();
    }

    protected function onPageChange(): void
    {
        $this->loadUsers();
    }

    protected function loadUsers(): void
    {
        $this->users = User::query()
            ->orderBy('name')
            ->paginate($this->perPage, ['*'], 'page', $this->page);
    }

    public function render()
    {
        return view('diffyne.user-list');
    }
}
blade
<div>
    {{-- Your data table --}}
    <table>
        @foreach($users as $user)
            <tr>
                <td>{{ $user->name }}</td>
                <td>{{ $user->email }}</td>
            </tr>
        @endforeach
    </table>

    {{-- Pagination links --}}
    @if($users && $users->hasPages())
        <div class="mt-6">
            {{ $users->links('diffyne::pagination') }}
        </div>
    @endif
</div>

That's it! The pagination is fully functional and reactive.

How It Works

Automatic Properties

The HasPagination trait provides these properties:

  • $page - Current page number (1-indexed, synced with URL)
  • $perPage - Items per page (default: 15)

Automatic Methods

  • nextPage() - Go to next page
  • previousPage() - Go to previous page
  • goToPage($page) - Jump to specific page
  • resetPage() - Go back to page 1

Lifecycle Hook

Implement onPageChange() to reload data when page changes:

php
protected function onPageChange(): void
{
    $this->loadUsers();
}

Basic Example

Component

php
<?php

namespace App\Diffyne;

use App\Models\Post;
use Diffyne\Component;
use Diffyne\Traits\HasPagination;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

class PostList extends Component
{
    use HasPagination;

    public ?LengthAwarePaginator $posts = null;

    public function mount(): void
    {
        $this->loadPosts();
    }

    protected function onPageChange(): void
    {
        $this->loadPosts();
    }

    protected function loadPosts(): void
    {
        $this->posts = Post::query()
            ->latest()
            ->paginate($this->perPage, ['*'], 'page', $this->page);
    }

    public function render()
    {
        return view('diffyne.post-list');
    }
}

View

blade
<div>
    <h2>Posts</h2>

    @if($posts && $posts->count() > 0)
        <div class="space-y-4">
            @foreach($posts as $post)
                <article class="border p-4">
                    <h3>{{ $post->title }}</h3>
                    <p>{{ $post->excerpt }}</p>
                </article>
            @endforeach
        </div>

        {{-- Pagination --}}
        @if($posts->hasPages())
            <div class="mt-6">
                {{ $posts->links('diffyne::pagination') }}
            </div>
        @endif
    @else
        <p>No posts found.</p>
    @endif
</div>

Customizing Items Per Page

Change Default

php
class PostList extends Component
{
    use HasPagination;

    public int $perPage = 25; // Default is 15

    // ...
}

Allow User Selection

php
use Diffyne\Attributes\Invokable;

#[Invokable]
public function setPerPage(int $perPage): void
{
    $this->perPage = $perPage;
    $this->resetPage(); // Go back to page 1
    $this->onPageChange();
}
blade
<select diff:model.live="perPage">
    <option value="10">10 per page</option>
    <option value="25">25 per page</option>
    <option value="50">50 per page</option>
</select>

When search changes, reset to page 1:

php
use Diffyne\Attributes\QueryString;

#[QueryString(keep: true)]
public ?string $search = '';

public function updated(string $property): void
{
    parent::updated($property);

    if ($property === 'search') {
        $this->resetPage();
        $this->loadUsers();
    }
}

protected function loadUsers(): void
{
    $query = User::query();

    if (!empty($this->search)) {
        $query->where('name', 'like', '%'.$this->search.'%');
    }

    $this->users = $query
        ->orderBy('name')
        ->paginate($this->perPage, ['*'], 'page', $this->page);
}

Pagination Helper Methods

The LengthAwarePaginator provides useful methods:

blade
{{-- Check if pagination is needed --}}
@if($users->hasPages())
    {{ $users->links('diffyne::pagination') }}
@endif

{{-- Display info --}}
<p>
    Showing {{ $users->firstItem() }} to {{ $users->lastItem() }} 
    of {{ $users->total() }} results
</p>

{{-- Check page position --}}
@if($users->onFirstPage())
    <p>You're on the first page</p>
@endif

@if($users->hasMorePages())
    <p>More pages available</p>
@endif

URL Synchronization

The $page property is automatically synced with the URL query string:

  • Page 1: /users
  • Page 2: /users?page=2
  • Page 3: /users?page=3

Users can:

  • Bookmark specific pages
  • Share links to specific pages
  • Use browser back/forward buttons
  • Refresh the page and stay on the same page

Complete Example: Search with Pagination

Component

php
<?php

namespace App\Diffyne;

use App\Models\User;
use Diffyne\Attributes\Invokable;
use Diffyne\Attributes\QueryString;
use Diffyne\Component;
use Diffyne\Traits\HasPagination;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

class UserList extends Component
{
    use HasPagination;

    public ?LengthAwarePaginator $users = null;
    
    #[QueryString(keep: true)]
    public ?string $search = '';
    
    public bool $loading = false;

    public function mount(): void
    {
        $this->loadUsers();
    }

    public function updated(string $property): void
    {
        parent::updated($property);

        if ($property === 'search') {
            $this->resetPage();
            $this->loadUsers();
        }
    }
    
    #[Invokable]
    public function clearSearch(): void
    {
        $this->search = '';
        $this->resetPage();
        $this->loadUsers();
    }

    protected function onPageChange(): void
    {
        $this->loadUsers();
    }

    protected function loadUsers(): void
    {
        $query = User::query();

        if (!empty($this->search)) {
            $query->where(function ($q) {
                $q->where('name', 'like', '%'.$this->search.'%')
                  ->orWhere('email', 'like', '%'.$this->search.'%');
            });
        }

        $this->users = $query
            ->orderBy('name')
            ->paginate($this->perPage, ['*'], 'page', $this->page);
    }

    public function render()
    {
        return view('diffyne.user-list');
    }
}

View

blade
<div class="user-list-container">
    {{-- Search --}}
    <div class="mb-4">
        <input 
            type="text" 
            diff:model.live.debounce.300="search"
            placeholder="Search users..."
            class="w-full px-4 py-2 border rounded-lg"
        >
    </div>

    {{-- Results count --}}
    @if($users)
        <p class="text-sm text-gray-600 mb-4">
            Showing {{ $users->firstItem() }} to {{ $users->lastItem() }} 
            of {{ $users->total() }} users
            @if(!empty($search))
                matching "{{ $search }}"
            @endif
        </p>
    @endif

    {{-- Users table --}}
    @if(!$users || $users->isEmpty())
        <p class="text-center py-8 text-gray-500">
            @if(empty($search))
                No users found.
            @else
                No users match "{{ $search }}"
            @endif
        </p>
    @else
        <table class="w-full">
            <thead>
                <tr>
                    <th>Name</th>
                    <th>Email</th>
                </tr>
            </thead>
            <tbody>
                @foreach($users as $user)
                    <tr diff:key="{{ $user->id }}">
                        <td>{{ $user->name }}</td>
                        <td>{{ $user->email }}</td>
                    </tr>
                @endforeach
            </tbody>
        </table>
    @endif

    {{-- Pagination --}}
    @if($users && $users->hasPages())
        <div class="mt-6">
            {{ $users->links('diffyne::pagination') }}
        </div>
    @endif
</div>

Best Practices

1. Always Reset Page on Filter Changes

php
public function updated(string $property): void
{
    if (in_array($property, ['search', 'category', 'status'])) {
        $this->resetPage(); // Important!
        $this->loadData();
    }
}

2. Use onPageChange() Hook

Don't call loadData() directly from navigation methods:

php
// ✅ Good
protected function onPageChange(): void
{
    $this->loadData();
}

// ❌ Avoid
#[Invokable]
public function nextPage(): void
{
    $this->page++;
    $this->loadData(); // Don't do this
}

3. Check for Empty Results

blade
@if($users && $users->count() > 0)
    {{-- Display data --}}
@else
    <p>No results found.</p>
@endif

4. Use diff:key for List Items

blade
@foreach($users as $user)
    <tr diff:key="{{ $user->id }}">
        {{-- Row content --}}
    </tr>
@endforeach

This ensures efficient DOM updates when paginating.

5. Show Loading States

blade
<div diff:loading class="opacity-50">
    {{-- Content --}}
</div>

Common Patterns

Filtering with Pagination

php
public ?string $status = 'all';
public ?string $category = null;

public function updated(string $property): void
{
    if (in_array($property, ['status', 'category'])) {
        $this->resetPage();
        $this->loadPosts();
    }
}

protected function loadPosts(): void
{
    $query = Post::query();

    if ($this->status !== 'all') {
        $query->where('status', $this->status);
    }

    if ($this->category) {
        $query->where('category_id', $this->category);
    }

    $this->posts = $query
        ->latest()
        ->paginate($this->perPage, ['*'], 'page', $this->page);
}

Sorting with Pagination

php
use Diffyne\Attributes\Invokable;

public string $sortBy = 'created_at';
public string $sortDir = 'desc';

#[Invokable]
public function sort(string $field): void
{
    if ($this->sortBy === $field) {
        $this->sortDir = $this->sortDir === 'asc' ? 'desc' : 'asc';
    } else {
        $this->sortBy = $field;
        $this->sortDir = 'asc';
    }
    
    $this->resetPage();
    $this->loadData();
}

protected function loadData(): void
{
    $this->items = Item::query()
        ->orderBy($this->sortBy, $this->sortDir)
        ->paginate($this->perPage, ['*'], 'page', $this->page);
}

Troubleshooting

Pagination Not Updating

Problem: Clicking pagination links doesn't change the page.

Solution: Make sure you've implemented onPageChange():

php
protected function onPageChange(): void
{
    $this->loadData(); // Reload data when page changes
}

Page Resets on Every Request

Problem: Always goes back to page 1.

Solution: Don't reset $page in mount() or updated() unless necessary:

php
// ❌ Wrong
public function mount(): void
{
    $this->page = 1; // Don't do this
    $this->loadData();
}

// ✅ Correct
public function mount(): void
{
    $this->loadData(); // Page is already 1 by default
}

URL Not Updating

Problem: Page number not in URL.

Solution: The $page property is automatically synced with URL via #[QueryString]. If it's not working, check that you're using the trait correctly.

Empty Results After Pagination

Problem: No data shown after changing pages.

Solution: Ensure onPageChange() calls your data loading method:

php
protected function onPageChange(): void
{
    $this->loadUsers(); // Make sure this method exists and works
}

Next Steps

Released under the MIT License.