Skip to content

Pagination

Most web frameworks like Django offers a pagination feature that allows you to query a subset of data and provides your users with the ability to navigate between pages of those results.

Because most of these paginators were designed for static applications, in a non-Liveblade app, each page navigation triggers a full browser visit to a new URL containing the desired page (?page=2).

However, when you use pagination inside a Liveblade component, users can navigate between pages while remaining on the same page. Liveblade will handle everything behind the scenes, including updating the URL query string with the current page.

All it takes to enable pagination in your component is to inherit from the liveblade.Paginator mixin — it's that effortless.

Basic usage

Below is the most basic example of using pagination inside a PostList component to only show 25 posts per page:

python
from pyblade import liveblade
from app.models import Post

class PostList(
    liveblade.Component,
    liveblade.Paginator
    ):
    
    def render(self):
        post_list = Post.objects.all()
        posts = self.paginate(post_list, 25) # Show 25 posts per page. 
        
        return self.view("post-list", {
            'posts': posts,
        })
blade
<div>
    <div>
        @for (post in posts)
            <!-- Render each post -.
        @endfor
    </div>

    {{ posts.links }}
</div>

As shown above, the paginate() method automatically handles slicing your dataset, and the links() helper renders a fully functional pagination UI — no additional setup required.

Customizing pagination URLs

By default, links generated by the paginator will match the current request's URI. However, the paginator's with_path method allows you to customize the URI used by the paginator when generating links. For example, if you want the paginator to generate links like http://example.com/admin/users?page=N, you should pass /admin/users/ to the with_path method:

python
users = self.paginate(User.objects.all(), 15).with_path("/admin/users")
...

Appending Query String values

You may append to the query string of pagination links using the appends method. For example, to append sort=votes to each pagination link, you should make the following call to appends:

python
users = self.paginate(User.objects.all(), 15).appends(sort="votes")

Disabling URL query string tracking

By default, Liveblade's paginator tracks the current page in the browser URL's query string like so: ?page=2.

If you wish to still use Liveblade's pagination utility, but disable query string tracking, you can do so using the without_query_string method on the paginator object:

python
posts = self.paginate(Post.objects.all(), 25).without_query_string()

Now, pagination will work as expected, but the current page won't show up in the query string. This also means the current page won't be persisted across page changes.

Displaying pagination results

When calling the paginate method, you will receive an instance of pyblade.liveblade.Paginator.

These objects provide several methods that describe the result set. In addition to these helper methods, the paginator instances are iterators and may be looped as a list. So, once you have retrieved the results, you may display the results and render the page links using PyBlade:

blade
<div class="container">
    @for(user in users)
        {{ user.name }}
    @endfor
</div>
 
{{ users.links() }}

The links() method will render the links to the rest of the pages in the result set. Each of these links will already contain the proper page query string variable. Remember, the HTML generated by the links method is compatible with the Tailwind CSS framework.

When the paginator displays pagination links, the current page number is displayed as well as links for the three pages before and after the current page. Using the on_each_side attribute, you may control how many additional links are displayed on each side of the current page within the middle, sliding window of links generated by the paginator:

blade
{{ users.links(on_each_side=5) }}

Customizing scroll behavior

By default, Liveblade's paginator scrolls to the top of the page after every page change.

You can disable this behavior by passing False to the scroll_to parameter of the links() method like so:

blade
{{ posts.links(scroll_to=False) }}

Alternatively, you can provide any CSS selector to the scroll_to parameter, and Liveblade will find the nearest element matching that selector and scroll to it after each navigation:

blade
{{ posts.links(scroll_to='#paginated-posts') }}

Multiple paginators

Sometimes you may need to render two separate paginators on a single screen that is rendered by your application. However, if both paginator instances use the page query string parameter to store the current page, the two paginator's will conflict.

To resolve this conflict, you may pass the name of the query string parameter you wish to use to store the paginator's current page via the third argument provided to the paginate, method.

python
users = self.paginate(User.objects.all(), 10, page_name="users")

To demonstrate the problem more clearly, consider the following ClientList component:

python
from pyblade.liveblade import Component, Paginator
form app.models import Client

class ClientList(Component, Paginator):

   def render(self):
        return self.view('client-list', {
            'clients': self.paginate(Client.objects.all(), 10)
        })

As you can see, the above component contains a paginated set of clients. If a user were to navigate to page 2 of this result set, the URL might look like the following:

http://example.com/?page=2

Suppose the page also contains a InvoiceList component that also uses pagination. To independently track each paginator's current page, you need to specify a name for the second paginator like so:

python
from pyblade.liveblade import Component, Paginator
from app.models import invoices

class InvoiceList(Component, Paginator):

   def render(self):
        return self.view('invoice-list', {
            'invoices': self.paginate(Invoice.objects.all(), 10, page_name="invoice_page")
        })

Now, because of the page_name parameter that has been added to the Paginator class, when a user visits page 2 of the invoices, the URL will contain the following:

https://example.com/customers?page=2&invoices-page=2

Resetting the page

When sorting or filtering results, it is common to want to reset the page number back to 1.

For this reason, Liveblade provides the self.reset_page() method, allowing you to reset the page number from anywhere in your component.

The following component demonstrates using this method to reset the page after the search form is submitted:

python
from pyblade.liveblade import Component, Paginator
from app.models import Post

class SearchPosts(Component, Paginator):
    query: str = ''

    def search(self):
        self.reset_page() 

    def render(self):
        post_list = Post.objects.filer(title__icontains=self.query)
        posts = self.paginate(post_list, 25)
        if self.query:
            posts.appends(q=self.query)
        
        return self.view("post-list", {"posts": posts})
blade
<div>
    <form b-submit="search">
        <input type="text" b-model="query">

        <button type="submit">Search posts</button>
    </form>

    <div>
        @for (post in posts)
            <!-- ... -.
        @endfor
    </div>

    {{ posts.links }}
</div>

Now, if a user was on page 5 of the results and then filtered the results further by pressing "Search posts", the page would be reset back to 1.

Available page navigation methods

In addition to reset_page(), Liveblade's Paginator provides other useful methods for navigating between pages programmatically from your component:

MethodDescription
set_page(page)Set the paginator to a specific page number
reset_page()Reset the page back to 1
next_page()Go to the next page
previous_page()Go to the previous page

Hooking into page updates

Liveblade allows you to execute code before and after a page is updated by defining either of the following methods inside your component:

python
from pyblade.liveblade import Component, Paginator
from app.models import Post

class PostList(Component, Paginator):

    def updating_page(self, page):
        """Runs before the page is updated for this component..."""
        pass

    def updated_page(self, page):
        """Runs after the page is updated for this component..."""
        pass

    def render(self):
        return view('post-list', {
            'posts': self.paginate(Post.objects.all(), 10)
        })

Named paginator hooks

The previous hooks only apply to the default paginator. If you are using a named paginator, you must define the methods using the paginator's name.

For example, below is an example of what a hook for a paginator named invoice_page would look like:

python
def updating_invoice_page(self, page):
    ...

General paginator hooks

If you prefer to not reference the paginator name in the hook method name, you can use the more generic alternatives and simply receive the pageName as a second argument to the hook method:

python
def updating_paginators(self, page, page_name):
   ...

def updating_paginators(self, page, page_name):
   ...

Customizing the pagination UI

Liveblade includes built-in support for paginated components out of the box, and it renders pagination links intelligently based on the CSS framework you’ve configured for your PyBlade project.

Tailwind or Bootstrap-Aware pagination

When rendering pagination links with Liveblade, the paginator checks the PyBlade project configuration file to determine which CSS framework you are using — Tailwind or Bootstrap. If one of these is configured, Liveblade will automatically render the appropriate pagination markup that matches your chosen framework.

For example:

  • If your project is configured to use Tailwind, Liveblade will render a Tailwind-compatible pagination component.
  • If you're using Bootstrap, the paginator will generate Bootstrap-friendly markup automatically.

NOTE

If your project doesn't explicitly configure a CSS framework, Tailwind pagination is used as the default fallback UI.

Using custom pagination template

While Liveblade intelligently adapts to Tailwind or Bootstrap, you can still take full control over the pagination UI.

The first approach is by specifying a custom template when calling the paginator's links() method in your component’s template:

html
{{ paginator.links('pagination.custom') }}

When rendering the pagination links, Liveblade will now look for a template at templates/pagination/custom.html.

You can also pass additional data to your custom template:

html
{{ paginator.links('pagination.custom', { 'foo': 'bar' }) }}

INFO

The paginator instance is automatically passed to the pagination template's context with the name paginator.

The second approach is to pass a template argument to the paginate() method inside your component :

python
posts = self.paginate(Post.objects.all(), 10, template="pagination.custom")

However, if you want to tweak the default Tailwind or Bootstrap pagination templates provided by Liveblade, the recommended way is to export the pagination templates to your project using the liveblade:stubs command with the --pagination flag :

bash
pyblade liveblade:stubs --pagination

After running this command, the following two files will be inserted into the templates/stubs/pagination directory:

Template file nameDescription
tailwind.htmlThe standard Tailwind pagination theme
bootstrap.htmlThe standard Bootstrap pagination theme

Once the files have been generated, you have complete control over them. When rendering pagination links using the paginator instance's links() method inside your template, Liveblade will automatically pick them up, instead of its own, based on your framework configuration.

This gives you the flexibility to adjust classes, markup, or structure to fit your design system.

Paginator instance methods

Each paginator instance provides additional pagination information via the following methods:

MethodDescription
paginator.count()Get the number of items for the current page.
paginator.total()Get the total number of items in the data store.
paginator.items()Get the items for the current page.
paginator.first_item()Get the result number of the first item in the results.
paginator.last_item()Get the result number of the last item in the results.
paginator.has_pages()Determine if there are enough items to split into multiple pages.
paginator.has_more_pages()Determine if there are more items in the data store.
paginator.per_page()The number of items to be shown per page.
paginator.last_page()Get the page number of the last available page.
paginator.current_page()Get the current page number.
paginator.on_first_page()Determine if the paginator is on the first page.
paginator.on_last_page()Determine if the paginator is on the last page.
paginator.get_url_range(start, end)Create a range of pagination URLs.
paginator.url(page)Get the URL for a given page number.
paginator.get_page_name()Get the query string variable used to store the page.
paginator.set_page_name(name)Set the query string variable used to store the page.
paginator.get_options()Get the paginator options.

Sample pagination template

Below is an unstyled sample of a simple Liveblade pagination view for your reference.

blade
<div>
    @if (paginator.has_pages())
        <nav role="navigation" aria-label="Pagination Navigation">
            <span>
                @if (paginator.on_first_page())
                    <span>Previous</span>
                @else
                    <button b-click="previous_page" b-loading.attr="disabled" rel="prev">Previous</button>
                @endif
            </span>

            <span>
                @if (paginator.on_last_page())
                    <span>Next</span>
                @else
                    <button b-click="next_page" b-loading.attr="disabled" rel="next">Next</button>
                @endif
            </span>
        </nav>
    @endif
</div>

As you can see, you can use Liveblade's page navigation helpers like self.next_page() directly inside your template by adding b-click="next_page" to buttons.