Yii2 ListView: Create Custom LinkPager Class - Part1

 ·  · 

Extending Yii2 default LinkPager widget class in order to add custom functions to Yii2 ListView pagination.

 

1 Goal

The goal of this demo is to create a custom Yii2 widget which extends yii\widgets\LinkPager.

Main feature of this custom pager widget:

  • add a page size dropDownList and a "Go to Page" input box to default pagination.
  • the browser will navigate to new URL when user select a new page size or input a "Go to page" number then press enter key.

yii2_custom_linkpager_class_widget

 

2 Yii2 Widget

Before moving on you'd better familiar with knowledge of creating a custom Yii2 widget. Otherwise you can read tutorials here:

 

3 Dig Into Default LinkPager Class

Let's look into the source code of Yii2 default LinkPager class: yii\widgets\LinkPager, GitHub link: yii\widgets\LinkPager.

we are interested in two places in the source codes: $pagination and renderPageButtons()

Following picture explains how LinkPager widget works.

yii2_linkpager_class

  1. $pagination is an instance of yii\data\Pagination, providing pagination model data, such as $totalCount.

  2. renderPageButtons() is the key method to render pagination link buttons.

So we can create a subclass of yii\widgets\LinkPager to do some custom rendering besides the default renderPageButtons().

custom_pagination_pager_subclass_of_LinkPager

 

4 Create New Widget

Fist of all, create folders, subfolders and main files for the new widget.

your_app (e.g. frontend)/
    - widgets
        - demopager/
            - DemoPager.php
            - DemoPagerAsset.php
            - assets/
                - js/
                    - demopager.js

yii2_custom_linkpager_class_folder

 

5 Pager Layout

I will give a pager_layout member variable to the new pager widget class. The run method will render view of widget according to configured pager_layout. Look into the main idea below.

// DemoPager.php
class DemoPager extends \yii\widgets\LinkPager
{
    public $pager_layout = '{pageButtons} {pageSizeList} {goToPage}';

    public function init()
    {
        parent::init();
    }

    public function run()
    {
        // if pageButtons, render pageButtons
        // if pageSizeList, render pageSizeList
        // if goToPage, render goToPage
    }
}

So we can add three functions to render sub sections of pager_layout separately, and then call them in run.

// DemoPager.php
class DemoPager extends \yii\widgets\LinkPager
{
    public $pager_layout = '{pageButtons} {pageSizeList} {goToPage}';

    public function init()
    {
        parent::init();
    }

    public function run()
    {
        return preg_replace_callback("/{(\\w+)}/", function ($matches) {
            $sub_section_name = $matches[1];
            $sub_section_content = $this->renderSection($sub_section_name);

            return $sub_section_content === false ? $matches[1] : $sub_section_content;
        }, $this->pager_layout);
    }

    protected function renderSection($name)
    {
        switch ($name) {
            case 'pageButtons':
                // Call inherited renderPageButtons() method
                return $this->renderPageButtons();
            case 'pageSizeList':
                // Render sub section, page size dropDownList
                return $this->renderPageSizeList();
            case 'goToPage':
                // Render sub section, go to page textInput
                return $this->renderGoToPage();
            default:
                return false;
        }
    }
}

I use preg_replace_callback to match sub section names in configured pager_layout, and render the sub section by calling renderSection immediately.

 

6 Render Page Size Dropdown Select

Rendering page size dropDownList is easy. First prepare a default page sizes array.

// DemoPager.php
class DemoPager extends \yii\widgets\LinkPager
{
    // ...
    private $pageSizeList = [5, 10, 20, 30];
    // ...
}

Then we should push the current page size into this array. Remember that the parent class yii\widgets\LinkPager own an instance of yii\data\Pagination: $pagination. $pagination can provide pagination model data, such as current page size, pagination query string parameter names, total page count and so on.

// DemoPager.php
class DemoPager extends \yii\widgets\LinkPager
{
    public $pager_layout = '{pageButtons} {pageSizeList} {goToPage}';
    private $pageSizeList = [5, 10, 20, 30];

    public function init()
    {
        parent::init();

        $currentPageSize = $this->pagination->getPageSize();

        // Push current pageSize to $this->pageSizeList,
        // unique to avoid duplicating
        if ( !in_array($currentPageSize, $this->pageSizeList) ) {
            array_unshift($this->pageSizeList, $currentPageSize);
            $this->pageSizeList = array_unique($this->pageSizeList);

            // Sort
            sort($this->pageSizeList, SORT_NUMERIC);
        }
    }
}

Now it is ready to render page size dropDownList. The values and texts of select options will be populated with $pageSizeList. I will also give a name attribute to the select. The value of name attribute will be $pageSizeParam of yii\data\Pagination. The default $pageSizeParam is "per-page".

yii2_custom_linkpager_class_name_attribute

Additionally, add a $sizeListHtmlOptions allowing for configuring HTML attributes of select.

// DemoPager.php
namespace frontend\widgets\demopager;

use yii\helpers\Html;

class DemoPager extends \yii\widgets\LinkPager
{
    public $pager_layout = '{pageButtons} {pageSizeList} {goToPage}';

    public $sizeListHtmlOptions = [];
    protected $_page_size_param = 'per-page';

    private $pageSizeList = [5, 10, 20, 30];

    public function init()
    {
        parent::init();

        $currentPageSize = $this->pagination->getPageSize();
        $this->_page_size_param = $this->pagination->pageSizeParam;

        // Push current pageSize to $this->pageSizeList,
        // unique to avoid duplicating
        if ( !in_array($currentPageSize, $this->pageSizeList) ) {
            array_unshift($this->pageSizeList, $currentPageSize);
            $this->pageSizeList = array_unique($this->pageSizeList);

            // Sort
            sort($this->pageSizeList, SORT_NUMERIC);
        }
    }

    private function renderPageSizeList()
    {
        return Html::dropDownList($this->_page_size_param,
            $this->pagination->getPageSize(),
            array_combine($this->pageSizeList, $this->pageSizeList),
            $this->sizeListHtmlOptions
        );
    }

    // ...
}

 

7 Render Jump To Input

Similarly, the "Go to page" input should have a name attribute, its value will be set by $this->pagination->pageParam.

// DemoPager.php
namespace frontend\widgets\demopager;

use Yii;
use yii\helpers\Html;

class DemoPager extends \yii\widgets\LinkPager
{
    public $pager_layout = '{pageButtons} {pageSizeList} {goToPage}';

    public $sizeListHtmlOptions = [];
    protected $_page_size_param = 'per-page';

    public $goToPageHtmlOptions = ['placeholder' => 'Go to page'];
    protected $_page_param = 'page';

    private $pageSizeList = [5, 10, 20, 30];

    public function init()
    {
        parent::init();

        $this->_page_size_param = $this->pagination->pageSizeParam;
        $this->_page_param = $this->pagination->pageParam;

        $currentPageSize = $this->pagination->getPageSize();

        // Push current pageSize to $this->pageSizeList,
        // unique to avoid duplicating
        if ( !in_array($currentPageSize, $this->pageSizeList) ) {
            array_unshift($this->pageSizeList, $currentPageSize);
            $this->pageSizeList = array_unique($this->pageSizeList);

            // Sort
            sort($this->pageSizeList, SORT_NUMERIC);
        }
    }

    private function renderGoToPage()
    {
        $current_page = 1;
        $params = Yii::$app->getRequest()->queryParams;
        if ( isset($params[$this->_page_param]) ) {
            $current_page = intval($params[$this->_page_param]);
            if ($current_page < 1) {
                $current_page = 1;
            } elseif ( $current_page > $this->pagination->getPageCount() ) {
                $current_page = $this->pagination->getPageCount();
            }
        }

        return Html::textInput($this->_page_param,
            $current_page,
            $this->goToPageHtmlOptions
        );
    }

    // ...
}

Now the whole DemoPager.php is as follow:

<?php
// DemoPager.php
namespace frontend\widgets\demopager;

use Yii;
use yii\helpers\Html;

class DemoPager extends \yii\widgets\LinkPager
{
    private $pageSizeList = [5, 10, 20, 30];

    public $pager_layout = '{pageButtons} {pageSizeList} {goToPage}';
    public $sizeListHtmlOptions = [];
    public $goToPageHtmlOptions = ['placeholder' => 'Go to page'];

    // e.g. &page=1&per-page=5
    // Pagination query string params name
    // I'd like to add underscore to vars' name to avoid any overriden
    protected $_page_param = 'page';
    protected $_page_size_param = 'per-page';


    public function init()
    {
        parent::init();

        $this->_page_param = $this->pagination->pageParam;
        $this->_page_size_param = $this->pagination->pageSizeParam;

        $currentPageSize = $this->pagination->getPageSize();

        // Push current pageSize to $this->pageSizeList,
        // unique to avoid duplicating
        if ( !in_array($currentPageSize, $this->pageSizeList) ) {
            array_unshift($this->pageSizeList, $currentPageSize);
            $this->pageSizeList = array_unique($this->pageSizeList);

            // Sort
            sort($this->pageSizeList, SORT_NUMERIC);
        }
    }

    public function run()
    {
        if ($this->registerLinkTags) {
            $this->registerLinkTags();
        }

        return preg_replace_callback("/{(\\w+)}/", function ($matches) {
            $sub_section_name = $matches[1];
            $sub_section_content = $this->renderSection($sub_section_name);

            return $sub_section_content === false ? $matches[1] : $sub_section_content;
        }, $this->pager_layout);
    }

    protected function renderSection($name)
    {
        switch ($name) {
            case 'pageButtons':
                // Call inherited renderPageButtons() method
                return $this->renderPageButtons();
            case 'pageSizeList':
                // Render sub section, page size dropDownList
                return $this->renderPageSizeList();
            case 'goToPage':
                // Render sub section, go to page textInput
                return $this->renderGoToPage();
            default:
                return false;
        }
    }

    private function renderPageSizeList()
    {
        return Html::dropDownList($this->_page_size_param,
            $this->pagination->getPageSize(),
            array_combine($this->pageSizeList, $this->pageSizeList),
            $this->sizeListHtmlOptions
        );
    }

    private function renderGoToPage()
    {
        $current_page = 1;
        $params = Yii::$app->getRequest()->queryParams;
        if ( isset($params[$this->_page_param]) ) {
            $current_page = intval($params[$this->_page_param]);
            if ($current_page < 1) {
                $current_page = 1;
            } elseif ( $current_page > $this->pagination->getPageCount() ) {
                $current_page = $this->pagination->getPageCount();
            }
        }

        return Html::textInput($this->_page_param,
            $current_page,
            $this->goToPageHtmlOptions
        );
    }
}

So far so good. The new DemoPager widget can now display page size select box and "Go to page" input box. But to make it work we still need some JavaScript code. We'll cover that in next part.