TYPO3: Migrate fluid pagination widget to paginator API

TYPO3 v10 deprecated and TYPO3 v11 removed all fluid widget code. The most used widget was probably the pagination widget. Since TYPO3 10.2 a new pagination API has been integrated into the TYPO3 core - let's take a look at how it can be used.

TYPO3 PaginatorInterface based Fluid Pagination

This is what we want to achieve:

The Basic Setup: TYPO3 Extbase Extension

I'm using a simple extbase extension which displays a list view of items in an array.

The extbase controller action basically looks like this (without any pagination):

    public function listAction()
    {
        $productIds = explode(',', $this->settings['items'] ?? '');
        $products = $this->productService->getProductsByIds($productIds);
        $this->view->assign('products', $products);
    }

Adding Pagination to the Extbase Controller

The main difference between pagination done via Fluid widget and the new Pagination API is that the pagination logic moves to our main controller.

The TYPO3 Core implements basic pagination with the ArrayPaginator object and the SimplePagination object. Extbase additionally provides the \TYPO3\CMS\Extbase\Pagination\QueryResultPaginator to allow pagination of Extbase repository query results.

In the example above, we have an array of $products, so we can choose to use the ArrayPaginator and SimplePagination:

    use TYPO3\CMS\Core\Pagination\ArrayPaginator;
    use TYPO3\CMS\Core\Pagination\SimplePagination;

    // ...

    /**
     * @param int $currentPage
     */
    public function listAction(int $currentPage = 1)
    {
        $productIds = explode(',', $this->settings['items'] ?? '');
        $products = $this->productService->getProductsByIds($productIds);
        $arrayPaginator = new ArrayPaginator($products, $currentPage, 8);
        $pagination = new SimplePagination($arrayPaginator);
        $this->view->assignMultiple(
            [
                'products' => $products,
                'paginator' => $arrayPaginator,
                'pagination' => $pagination,
                'pages' => range(1, $pagination->getLastPageNumber()),
            ]
        );
    }

Both the paginator and the pagination are assigned to the view, as we need both of them to display the pagination and paginate the items. Additionally, we want to render a link to every available page, that's why we add an array containing the available page numbers via the PHP range() function.

Technical Background: Pagination in TYPO3 is based on the two interfaces:
\TYPO3\CMS\Core\Pagination\PaginatorInterface
\TYPO3\CMS\Core\Pagination\PaginationInterface 
The Paginator is responsible for the pagination logic, for example slicing the available values according to the current page or calculating the number of available pages. The Pagination contains the additional logic necessary to render a paginator, for example returning the next page number, or the currently displayed records.

Implementing Pagination in TYPO3 Fluid

To render our list of products paginated, we use the paginator variable.

<f:for as="product" each="{paginator.paginatedItems}" iteration="iterator">
    <f:render partial="Product/BoxStandard.html" arguments="{product: product}" />
</f:for>

The paginator should be in a partial, so we can reuse it:

<f:render partial="Utility/Paginator.html" arguments="{pagination: pagination, pages: pages, paginator: paginator}" />

Here is the partial I'm using with the following features:

  • Displays "first page" and "last page" links (and disables them if unavailable)
  • Displays "next page" and "previous page" links (and disables them if unavailable)
  • Displays link for each page with "active" highlighting
  • Uses MaterializeCSS based pagination styles
<ul class="pagination pagination-block">
  <f:if condition="{pagination.previousPageNumber} && {pagination.previousPageNumber} >= {pagination.firstPageNumber}">
    <f:then>
      <li class="waves-effect">
        <a href="{f:uri.action(action:actionName, arguments:{currentPage: 1})}" title="{f:translate(key:'pagination.first')}">
          <i class="material-icons">first_page</i>
        </a>
      </li>
      <li class="waves-effect">
        <a href="{f:uri.action(action:actionName, arguments:{currentPage: pagination.previousPageNumber})}" title="{f:translate(key:'pagination.previous')}">
          <i class="material-icons">chevron_left</i>
        </a>
      </li>
    </f:then>
    <f:else>
      <li class="disabled"><a href="#"><i class="material-icons">first_page</i></a></li>
      <li class="disabled"><a href="#"><i class="material-icons">chevron_left</i></a></li>
    </f:else>
  </f:if>
  <f:for each="{pages}" as="page">
    <li class="{f:if(condition: '{page} == {paginator.currentPageNumber}', then:'active', else:'waves-effect')}">
      <a href="{f:uri.action(action:actionName, arguments:{currentPage: page})}">{page}</a>
    </li>
  </f:for>

  <f:if condition="{pagination.nextPageNumber} && {pagination.nextPageNumber} <= {pagination.lastPageNumber}">
    <f:then>
      <li class="waves-effect">
        <a href="{f:uri.action(action:actionName, arguments:{currentPage: pagination.nextPageNumber})}" title="{f:translate(key:'pagination.next')}">
          <i class="material-icons">chevron_right</i>
        </a>
      </li>
      <li class="waves-effect">
        <a href="{f:uri.action(action:actionName, arguments:{currentPage: pagination.lastPageNumber})}" title="{f:translate(key:'pagination.last')}">
          <i class="material-icons">last_page</i>
        </a>
      </li>
    </f:then>
    <f:else>
      <li class="disabled"><a href="#"><i class="material-icons">chevron_right</i></a></li>
      <li class="disabled"><a href="#"><i class="material-icons">last_page</i></a></li>
    </f:else>
  </f:if>
</ul>

We can also add a line showing the currently displayed records:

{pagination.startRecordNumber} - {pagination.endRecordNumber}

For another example, you can have a look at the TYPO3 ExtensionManager TER table rendering.

More Info