Переключение числа элементов на странице в CListView

Показывать по

Для вывода ленты записей на страницу в Yii имеется очень удобный готовый виджет CListView. Совместно с провайдером данных он позволяет выводить элементы с разбивкой на страницы и сортировкой. Но при разработке некоторых интернет-магазинов и всевозможных каталогов часто возникает необходимость в переключении числа элементов на странице. Попробуем добавить меню «Выводить по: 10 20 30» в нашу ленту записей.

Конечно же, в качестве быстрого решения задачи можно нагенерировать и вывести ссылки прямо в коде представления.

Ручная генерация ссылок в коде представления

Мы можем разместить переключатель количества элементов перед виджетом или после него:

<?php
<h1>Блог</h1>
 
<?php foreach (array(10, 20, 30) as $count){        
    $params = array_replace($_GET, array('size'=>$count));
    if (isset($params['page'])) unset($params['page']);    
    $links[] = CHtml::link($count, $this->createUrl('', $params));
} ?>
 
<p>Выводить по: <?php echo implode(', ', $links); ?></p>
 
<?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
)); ?>

Здесь мы в каждой итерации цикла просто берём пришедшие параметры в $params = $_GET, заменяем $params['size'] на текущее значение (10, 20 или 30) и с помощью вызова

$this->createUrl('', $params)

генерируем ссылку на этот же маршрут. Не забываем сбрасывать номер текущей страницы в ссылке вызовом unset($params['page']), чтобы просмотр элементов всегда начинался с первой страницы.

Вызов метода CController::createUrl с пустой строкой эквивалентен вызову с непосредственным указанием текущего маршрута

$this->createUrl($this->route, $params)

В обоих случаях сгенерируется ссылка на текущий маршрут.

Таким образом, все параметры, пришедшие в $_GET (сортировка, фильтры), останутся нетронутыми. Только в каждой сгенерированной ссылке удалится параметр page и изменится параметр size.

Далее мы в строке

<?php
<p>Выводить по: <?php echo implode(', ', $links); ?></p>

выводим поясняющий текст и ссылки через запятую.

Чтобы клики по ним корректно обрабатывались контроллером, нам нужно присвоить принятые значения компоненту CPagination нашего провайдера данных:

$dataProvider = new CActiveDataProvider('Post', array(
    'pagination'=>array(
        'pageSize'=>Yii::app()->request->getQuery('size', 10),
        'pageVar'=>'page',
    ),
));

Если параметр запроса $_GET['size'] не задан, то значение установится по умолчанию в 10 элементов на страницу.

Теперь мы можем попробовать кликнуть по этим ссылкам и увидеть изменение числа элементов на странице.

Недостатки такого подхода:

  • Увеличивается размер кода представления;
  • Необходимость копирования и слежения за кодом в каждом представлении;

Для устранения этих недостатков можно перенести этот код в отдельный фрагмент protected/views/_sizer.php и включать его через renderPartial:

<?php
<h1>Блог</h1>
<?php $this->renderPartial('//_sizer'); ?>
<?php $this->widget('zii.widgets.CListView', array(...)); ?>

или вынести в виджет Sizer и использовать его:

<?php
<h1>Блог</h1>
<?php $this->widget('Sizer'); ?>
<?php $this->widget('zii.widgets.CListView', array(...)); ?>

Но никакой из этих способов не избавит нас от ещё одного более важного недостатка. Проблема в том, что при установке опции 'ajaxUpdate'=>true у CListView наш код не будет перезагружаться при манипуляции со списком. Это приведёт к тому, что наши ссылки не будут обновляться при изменении порядка сортировки элементов и при фильтрации. То есть этот подход не будет корректно работать в Ajax режиме.

Для корректной работы наш код нужно поместить внутрь CListView.

Встраивание переключателя числа элементов в CListView

Рассмотрим немного другой вызов виджета:

$this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'template'=>"{summary}\n{sorter}\n{items}\n{pager}"
));

Обратите внимание на параметр template. Он задаёт последовательность вывода блоков и контент между ними. По значению этого параметра мы видим, что внутри виджета уже имеется функционал подключения паджинатора {pager} и сортировщика {sorter}.

Если углубиться в исходный код виджета, то можно заметить, что вместо заготовок в фигурных скобках подставляются результаты выполнения имеющихся в классе одноимённых методов:

class CListView extends CBaseListView
{
    public function renderSummary(){echo ...}
    public function renderSorter(){echo ...}
    public function renderItems(){echo ...}
    public function renderPager(){echo ...}
}

По аналогии с уже имеющимися, мы можем создать свой метод renderSizer()? используя наследование:

class ListView extends ClistView
{
    public function renderSizer()
    {
        $links = array();
        foreach (array(10, 20, 30) as $count)
        {        
            $params = array_replace($_GET, array('size'=>$count));
            if (isset($params['page']))
                unset($params['page']);
 
            $links[] = CHtml::link($count, Yii::app()->controller->createUrl('', $params));
        }        
        echo '<p>Выводить по: ' . implode(', ', $links) . '</p>';
    }
}

Не забудем взять наш усовершенствованный провайдер в контроллере:

$dataProvider = new CActiveDataProvider('Post', array(
    'pagination'=>array(
        'pageSize'=>Yii::app()->request->getQuery('size', 10),
        'pageVar'=>'page',
    ),
));

Заменим теперь класс виджета на 'ListView' и добавим заготовку {sizer} в шаблон:

$this->widget('ListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'template'=>"{summary}\n{sizer}\n{sorter}\n{items}\n{pager}"
));

Теперь наш фрагмент выводится внутри самого виджета и вместе с ним обновляет ссылки при Ajax манипуляциях.

Рефакторинг для повторного использования

На данный момент мы сделали вполне рабочее решение. Но перед тем, как делиться им с соседом, нам нужно изрядно его доработать.

Заметьте, что у нас варианты array(10, 20, 30), строки 'size' и 'page' вписаны прямо в код. Конечно же, сосед может взять этот код и переписать, но что если нам или ему надо в разных разделах сайта выводить разные количества элементов? Поэтому лучше вынести все значения в настраиваемые параметры с установками по умолчанию.

Имя параметра 'page' мы взяли таким для удобства, но мы можем избавиться от его указания, так как каким бы оно ни было, мы можем получить его прямо из dataProvider. Также обернём наши ссылки тегом <div> с настраиваемым классом и уберём ссылку с текущего варианта.

В итоге мы получили легко настраиваемый класс:

class DSizerListView extends ClistView
{
    /**
     * @var string GET attribute
     */
    public $sizerAttribute = 'size';
 
    /**
     * @var array items per page sizes variants
     */
    public $sizerVariants = array(10, 20, 30);
 
    /**
     * @var string CSS class of sorter element
     */
    public $sizerCssClass = 'sizer';
 
    /**
     * @var string the text shown before sizer links. Defaults to empty.
     */
    public $sizerHeader = 'Show by: ';
 
    /**
     * @var string the text shown after sizer links. Defaults to empty.
     */
    public $sizerFooter = '';
 
    public function renderSizer()
    {
        $pageVar = $this->dataProvider->getPagination()->pageVar;    
        $pageSize = $this->dataProvider->getPagination()->pageSize;    
 
        $links = array();       
        foreach ($this->sizerVariants as $count)
        {
            $params = array_replace($_GET, array($this->sizerAttribute => $count));
 
            if (isset($params[$pageVar])) 
                unset($params[$pageVar]);
 
            if ($count == $pageSize)
                $links[] = $count;
            else            
                $links[] = CHtml::link($count, Yii::app()->controller->createUrl('', $params));
        }        
        echo CHtml::tag('div', array('class'=>$this->sizerCssClass), $this->sizerHeader . implode(', ', $links));
        echo $this->sizerFooter;
    }
}

и можем задавать любые параметры непосредственно при вызове:

<?php
<?php $this->widget('DSizerListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'template'=>"{sizer}\n{summary}\n{items}\n{pager}",
    'sizerVariants'=>array(10, 20, 30),
    'sizerAttribute'=>'size',
    'sizerHeader'=>'Выводить по: ',
)); ?>

Сейчас установка текущего значения числа элементов pageSize на страницу из параметров URL запроса происходит в момент создания провайдера данных в контроллере:

$dataProvider = new CActiveDataProvider('Post', array(
    'criteria'=>$criteria,
    'pagination'=>array(
        'pageSize'=>Yii::app()->request->getQuery('size', 10),
    ),
));

Здесь имя параметра 'size' должно совпадать с именем в опции sizerAttribute виджета. Этот процесс можно удалить из контроллера, оставив конструкцию по умолчанию

$dataProvider = new CActiveDataProvider('Post', array(
    'criteria'=>$criteria,    
);

и поместить присвоение непосредственно в метод инициализации виджета:

class ListView extends ClistView
{
    public function init()
    {
        if (!isset($this->sizerVariants[0]))
            $this->sizerVariants = array(10);
 
        $pageSize = Yii::app()->request->getQuery($this->sizerAttribute, $this->sizerVariants[0]);
        $this->dataProvider->pagination->pageSize = $pageSize;
 
        parent::init();
    }
    ...
}

Таким образом, теперь соседу нужно только поменять значения опций. Больше не придётся ничего менять в коде контроллеров или моделей, а также не нужно будет следить за именами параметров.

Теперь можно подробнее изучить код метода renderSorter и доработать наш виджет по его образцу. А именно, вывести ссылки в ненумерованном списке, добавить параметр enableSizer и ввести некоторые проверки на достаточное число элементов:

Код на GitHub

Эту замену стандартного виджета можно теперь использовать в любом разделе сайта:

$dataProvider = new CActiveDataProvider('Post', array(
    'criteria'=>$criteria,    
);
<?php
<?php $this->widget('DSizerListView', array(
    'dataProvider'=>$dataProvider,
    'itemView'=>'_view',
    'template'=>"{sizer}\n{summary}\n{items}\n{pager}",
    'sizerVariants'=>array(10, 20, 30),
    'sizerAttribute'=>'size',
    'sizerCssClass'=>'sorter',
    'sizerHeader'=>'Show per page: ',
)); ?>

Чтобы не копировать такие массивы параметров в каждое представление, можно вынести общие значения в скин или в параметры компонента widgetFactory в конфигурационном файле (как мы делали это ранее).

Теперь мы научились создавать любые дополнительные блоки внутри виджета. А так как класс CGridView тоже наследуется от CBaseListView, то аналогично мы можем добавлять блоки и в грид.

Комментарии

 

Виталий Иванов

Отличная работа!

Ответить

 

TwiX

Автор молодец!
У самого переключалка была во вьюшке и постоянно переносил эту грамаду кода, руки не доходили переписать)) Теперь все просто! Спасибо!

Ответить

 

Иван

Отличная статья...
Но есть вопрос, использую данный способ для Grid'a где так же есть пагинация и все остальное, страница загружается sizer присутствует и функционирует а вот при переходе на последующие страницы sizer уже отсутствует, подскажите в чем может быть ошибка???
и как подогнать данный sizer как форму select???

Ответить

 

Дмитрий Елисеев

C CGridView использовать не пробовал, но, наверное, может помочь отключение ajaxUpdate.

Ответить

 

Степан – codergames.ru

Спасибо за статью, очень полезная.

Ответить

 

Сергей

Я создаю класс, кидаю файл ListView.php в папку site/protected/components которая у меня прописана в main.php для импорта, но получаю ошибку include(CListView.php): failed to open stream: No such file or directory

Судя по всему он не может найти класс, от корого я собираюсь наследоваться в классе ListView, тоесть сам CListView

Ответить

 

Дмитрий Елисеев

Да. Перед вашим классом добавьте строку:

Yii::import('zii.widgets.CListView');
Ответить

 

Snizhok

Было бы неплохо еще и обновление через ajax допилить

Ответить

 

alex

Спасибо огромное автору за качественную статью!

Ответить

Оставить комментарий

Войти | Завести аккаунт | Войти через


(никто не увидит)





Можно использовать теги <p> <ul> <li> <b> <i> <a> <pre>