DToggleColumn: Колонка-переключатель для CGridView

При администрировании многих разделов сайта возникла необходимость работы с логическими переключаемыми атрибутами модели (опубликована новость или нет, прочитано сообщение или нет и т.п.). В сети нашлось решение использовать колонку из чекбоксов с автосохранением данных Ajax запросом по событию onChange(). Эстетически выводить несколько колонок чекбоксов было не очень красиво. Было решено для наглядности сделать столбик из иконок-ссылок.
Сначала код вывода колонки «Опубликовано» был таким:
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider'=>$model->search(), 'filter'=>$model, 'columns'=>array( 'date', 'title', array( 'name'=>'public', 'header'=>'О', 'filter'=>array(1=>'Опубликовано', 0=>'Не опубликовано'), 'htmlOptions'=>array('style'=>'width:30px;text-align:center'), 'value' => function($data) { $url = Yii::app()->controller->createUrl('toggle', array('id'=>$data->id, 'param'=>'public', 'token'=>Yii::app()->request->csrfToken)); $src = Yii::app()->request->baseUrl . ($data->public ? '/images/yes.png' : '/images/no.png'); $title = $data->public ? 'Опубликован' : 'Не опубликован' return CHtml::link(CHtml::image($src), $url, array('title'=>$title)); }, 'type' => 'raw', ), ) ));
Здесь в зависимости от текущего значения атрибута подставлялась определённая иконка со своей подсказкой. Переключение осуществлялось простым переходом по ссылке (GET-запросом) и редиректом обратно, поэтому для безопасности в обработчик actionToggle() приходилось передавать токен и вручную сравнивать его значение с текущим.
Этот код выглядел громоздко. При выполнении очередного проекта обнаружилось, что на хостинге стоит PHP 5.2 и от анонимных функций пришлось отказаться. Преобразовать эту анонимную функцию в строку, пригодную для обработки функцией eval() было затруднительно, поэтому весь код перекочевал в метод отдельного класса-хэлпера.
Это действовало, но было неким костылём, так как в отличие от стандартной кнопки «Удалить» работало по GET-запросу, а также не было обновления грида по Ajax. Для устранения этих недостатков на основе стандартной колонки CButtonColumn был реализован класс колонки-триггера DToggleColumn.
В простейшем случае для отображения текущего статуса в колонке необходимо указать атрибут модели и, при необходимости, адреса иконок. В такой ячейке будет выведена нужная иконка в зависимости от значения указанного атрибута модели. Этот пример можно использовать для пометки важных сообщений в списке обратной связи или срочных заказов в гриде интернет-магазина:
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider'=>$model->search(), 'filter'=>$model, 'columns'=>array( 'date', 'title', array( 'class'=>'DToggleColumn', // поле модели 'name'=>'important', // иконка для значения 1 или true 'onImageUrl' => Yii::app()->request->baseUrl . '/images/important.png', // иконка для значения 0 или false 'offImageUrl' => Yii::app()->request->baseUrl . '/images/spacer.gif', // убираем генерацию ссылки по умолчанию 'linkUrl'=>false, ), array( 'class'=>'CButtonColumn', ), ), ));
Здесь для несрочных заказов мы установили пустое изображение. Для скрытия ячейки лучше использовать параметр visible:
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider'=>$model->search(), 'filter'=>$model, 'columns'=>array( 'date', 'title', array( 'class'=>'DToggleColumn', // поле модели 'name'=>'important', // иконка для значения 1 или true 'onImageUrl' => Yii::app()->request->baseUrl . '/images/important.png', // убираем генерацию ссылки по умолчанию 'linkUrl'=>false; // отображать ли ячейку 'visible'=>'$data->important', ), array( 'class'=>'CButtonColumn', ), ), ));
Для колонок «Опубликовано» «На главной странице» и похожих нужно задать ссылку на обработчик linkUrl
<!-- list.php --> <h1>Записи блога</h1> $this->renderPartial('_grid',array('model'=>$model));
<!-- _grid.php --> <?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider'=>$model->search(), 'filter'=>$model, 'columns'=>array( 'date', 'title', // иконка со ссылкой по умолчанию на экшен toggleAction($id, $attribute) текущего контроллера array( 'class'=>'DToggleColumn', // атрибут модели 'name'=>'public', // заголовок столбца 'header'=>'О', // запрос подтвердждения (если нужен) 'confirmation'=>'Изменить статус публикации?', // фильтр 'filter'=>array(1=>'Опубликованные', 0=>'Не опубликованные'), // alt для иконок (так как отличается от стандартного) 'titles'=>array(1=>'Опубликовано', 0=>'Не опубликовано'), 'htmlOptions'=>array('style'=>'width:30px'), ), // иконка с другой ссылкой array( 'class'=>'DToggleColumn', 'name'=>'inhome', 'header'=>'Г', 'filter'=>array(1=>'На главной', 0=>'Не на главной'), // своя ссылка для переключения состояния 'linkUrl'=>'Yii::app()->controller->createUrl("customToggle", array("id"=>$data->id, "param"=>"inhome"))', 'htmlOptions'=>array('style'=>'width:30px'), ), array( 'class'=>'CButtonColumn', ), ), ));
Действие-обработчик actionToggle() весьма стандартное и похоже на действие для actionDelete(). Оно содержит проверку имени атрибута по списку разрешённых и, соответственно, само переключение значения:
class PostController extends Controller { // ... public function filters() { return array( 'postOnly + delete, toggle', ); } public function actionAdmin() { $model = new Post('search'); $model->unsetAttributes(); if(isset($_GET['Post'])) $model->attributes=$_GET['Post']; if (Yii::app()->request->isAjaxRequest) { $this->renderPartial('_grid', array( 'model'=>$model, )); } else { $this->render('list', array( 'model'=>$model, )); } } public function actionToggle($id, $attribute) { if (!in_array($attribute, array('public', 'inhome'))) throw new CHttpException(400, 'Некорректный запрос'); $model = $this->loadModel($id); $model->$attribute = $model->$attribute ? 0 : 1; $model->save(); if (!Yii::app()->request->isAjaxRequest) $this->redirect(isset($_POST['returnUrl']) ? $_POST['returnUrl'] : array('admin')); } // ... public function loadModel($id) { // ... } }
Теперь ячейки нашей колонки будут отображать статус и полноценно переключать значения полей по щелчку.
МаксимЗдравствуйте !
Не могли бы Вы дать ссылку на упомянутое в самом начале решение с чекбоксами ?
Хотя бы для сравнения с Вашим, которое мне очень нравится, но все-таки хотелось бы иметь возможность групповых toggle/untoggle.
Елисеев ДмитрийМожно взять обычную ячейку, вписать в неё код чекбокса и указать тип 'row':
array( 'name'=>'blocked', 'header'=>'Блокирован', 'filter'=>array(1=>'Заблокирован', 0=>'Нет'), 'value'=>'CHtml::checkBox("Blocked", $data->blocked, array("value"=>$data->id))', 'type'='row', ),
МаксимЗдравствуйте !
Пытался воспользоваться вашим компонентом.
Работает до тех пор, пока мне не нужно задавать параметр linkUrl.
'linkUrl'=>'Yii::app()->controller->createUrl("block", array("id"=>$data->id, "param"=>"blocked"))',При этом возникает ошибка
Что это может значить ?
Дмитрий ЕлисеевЗдравствуйте. В вашем фреймворке не нашёлся класс CJavaScriptExpression. Он доступен с версии 1.1.11.
МаксимСпасибо.
Обновил до текущей версии.
Работает
МаксимСкажите пожалуйста, можно ли использовать DToggleColumn, если поле, с которым она связана, может принимать значения не только 0 и 1, но 2,3,4 и т.д. ?
0 трактуется как false, все остальные значения - true.
Дмитрий ЕлисеевМожно, так как там простое условие с такой же трактовкой
МаксимА как в этом случае быть со свойствами filter и titles ?
Дмитрий ЕлисеевБудут нормально работать. Только значки и titles будут также отображать всего два состояния (Активно/Нет), а filter нормально работает сам по себе напрямую с провайдером данных.
МаксимУвы, увы ...
Во время исполнения в строке 178 DToggleColumn.php возникает ошибка Undefined offset: 9
Ладно, с этим я справился, заменив $title = $this->titles[(int)$value]; на $title = $this->titles[($value)?1:0];
Но с фильтром проблема серьезнее
Это, конечно, сугубо моя проблема, ибо поле num есть вычисляемое и действительно отсутствует в запросе, сформированном автоматически при выборе значения из списка 'filter'=>array(1=>'full', 0=>'empty').
Тем не менее, насколько я понимаю, невозможно прописать все возможные значения данного поля при задании 'filter', так что от его использования придется отказаться.
Дмитрий ЕлисеевВ свежей версии многое переделано и Undefined offset не выскочит. А с фильтром без поля действительно не получится с любой колонкой (не только с моей). Да и DToggleColumn не для этого изначально сделан.
lareinнужно использовать все возможности Yii, поэтому вместо:
if(!Yii::app()->request->isPostRequest) throw new CHttpException(400, 'Некорректный запрос');После генерации CRUD контроллера я бы дописал так:
public function filters() { return array( 'postOnly + delete, toggle', ); }
Дмитрий ЕлисеевДа-да. Я тоже использую фильтр postOnly. А это просто пример.
lareinну как вариант, конечно использовать проверку в контроллере, но и про postOnly нужно упомянуть, а то мало ли кто прочитает статью)
Дмитрий ЕлисеевПонятно. Поменял.
foreachДоброго времени суток, Дмитрий. Подскажите пожалуйста как правильно сделать две колонки с переключателями.
Пока не получилось. Добавил вторую колонку по примеру вашему. Но при клике на ссылке(иконке) уходит два пост запроса. Если вторую колонку убираю, то первая со статусом работает нормально.
Дмитрий ЕлисеевУ меня нормально работает:
array( 'class'=>'DToggleColumn', 'header'=>'Опубликован', 'name'=>'public', ), array( 'class'=>'DToggleColumn', 'header'=>'На главной', 'name'=>'inhome', ),
Дмитрий ЕлисеевВ текущей версии 1.1 классы уже генерировались через rand. В версии 1.2 они генерируются через автоинкремент, чтобы не терялась привязка скриптов при включенном ajaxUpdate.
PavelОтличное решение! Все работает!
Но есть один вопрос. Как можно по запросу организовать полное ajax обновление грида? Естественно без перезагрузки всей страницы.
Не хочется лепить фрэймы и так же не хочется это делать в попапе.
Если дадите хотя бы направление в какую сторону копать - буду признателен.
Дмитрий ЕлисеевВ старой версии было полное обновление посредством вызова
$.fn.yiiGridView.update('id_грида');Можете взять фрагмент оттуда.
PavelОгромное спасибо!
АлексейПолучается для каждой такой колонки надо писать свой экшн?
Дмитрий ЕлисеевМожно сделать один ToggleAction (или взять из комплекта действий) и подключать к каждому контроллеру.
АлексейДобрый день, Дмитрий. Спасибо за ответ. То есть в ToggleAction надо анализировать какой параметр был изменен, я правильно понимаю?
И еще вопрос. А как можно сделать то же самое, но без иконок, а с обычными чекбоксами?
Дмитрий ЕлисеевДа, в ToggleAction передаётся имя параметра.
Вместо иконки CHtml::image нужно вывести чекбокс CHtml::checkBox и заменить JavaScript обработчик с onClick на onChange.
КатяЗдравствуйте! Хочу сделать lazy load для грида. В самом виджете стоит
$model->search() задается в модели в одноименной функции search. Там как раз идет выборка данных, но если я допишу limit для lazy load там - get запрос не сработает. А если пишу в actionAdmin() в контроллере или отдельном экшене тоже почему-то не работает. Подскажите, пожалуйста, как это правильно сделать.
Дмитрий ЕлисеевА как именно вы там limit вписываете?
КатяСейчас limit удалось передать параметром в search:
public function search($param = array()) { $criteria=new CDbCriteria($param); $criteria->compare('id',$this->id); $criteria->compare('name',$this->name,true); $criteria->compare('description',$this->description,true); $criteria->compare('additional1',$this->additional1,true); return new CActiveDataProvider($this, array( 'criteria'=>$criteria, 'pagination'=>false, )); }И указав его в DataProvider в виджете самого грида:
'dataProvider'=>$model->search(array('limit'=>$limit))Получить результат из GET хотела в actionAdmin (у меня в админе формируется грид), но вместе с ним идет html код страницы, даже, если делаю проверку на аякс. Поэтому пока ловлю действие в отдельном экшене.
Передаю вот так:
var limit = 10; $(document).on('scroll', function(){ $.ajax({ type: "GET", url: '/index.php?r=article/lazyload', data: {limit:limit}, success:function(data){ alert(data); limit += 10; $('<p>'+data+'</p>').appendTo('.test1'); } }) });Подскажите, как получить результат в actionAdmin?
Дмитрий ЕлисеевДозагрузку я реализовывал у себя так.
ЖанЗдравствуйте,
подскажите где найти _grid.php?
как через ajax можно обновить рисунок? Например, после того как я нажал published чтобы рисунок автоматический показал unpublished?
Спасибо
Дмитрий ЕлисеевВ листинге есть _grid.php.
Рисунок сам обновляется.
Жанспасибо, получилось
Vit AndНе получилось вывести записи у которых «Опубликовано» «На главной странице» = true.
Что сделал:
1) В PostController.php->actionIndex() записал код:
$model=new Post('search'); $this->render('index', array( 'model'=>$model, ));2) В index.php вместо ...CListView... записал
$this->renderPartial('_grid',array('model'=>$model));3) В _grid.php - код, согласно приведенного выше в статье.
В итоге получил обратную картину: вижу записи у которых «Опубликовано»=false AND «На главной странице»=false.
Что сделать, что бы результат был правильным (показаны записи у которых «Опубликовано» «На главной странице» = true) ?
Спасибо.
Дмитрий Елисеев$model=new Post('search'); $model->unsetAttributes(); $model->public=1; $model->inhome=1; $this->render('index', array( 'model'=>$model, ));
Vit AndСпасибо. Все получилось.
AndreyДобрый день,
Не подскажите, как изменить ссылку в этом переключателе с текущего контроллре на сторонний.
Необходимо для переключеня состояния в гриде связанных данных
AndreyПробовал так:
'linkUrl'=>'/sadmin/food/toggle?id='.$model->id.'&attribute=exist',
Но в гриде выходят ошибки:
Parse error: syntax error, unexpected '/' in C:\OpenServer\domains\hotel\framework\base\CComponent.php(612) : eval()'d code on line 1
AndreyРешено
AndreyНе подскажите, почему при фильтре по показывать/не показывать опубликованные может вываливаться ошибка: cgridview ReferenceError: afterAjaxUpdate is not defined
ИванЗдравствуйте, Дмитрий!
Мучает меня вопрос по фильтрации GridView. По умолчанию фильтрация происходить по событию "change". А как сделать так чтобы фильтрация производилась по событию "input"?
Нашел в скрипте yii.gridView.js строчку:
Меняю значение change.yiiGridView на input.yiiGridView - результат становится похож на требуемый но с поля в процессе ввода постоянно пропадает фокус.
Может в Yii2 есть какая-то стандартная возможность заставить фильтрацию работать по событию input?
Дмитрий ЕлисеевВроде есть только это.