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.

Код на GitHub

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

<?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)
    {
        // ...
    }
}

Теперь ячейки нашей колонки будут отображать статус и полноценно переключать значения полей по щелчку.

Другие статьи

Довольно часто возникает необходимость проследить время исполнения некоторых фрагментов программного кода и отследить особо медленные участки для их последующей оптимизации. Имеющиеся отладочные расширения не проникают внутрь ваших файлов, а чаще выводят только общее время выполнения скрипта. Для локального рефакторинга и оптимизации они не подходят.

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

В теме о событиях натолкнулся на вопрос о том, как имея объект комментария на сайте получить ссылку на материал, к которому этот комментарий оставлен, при условии, что комментарии могут иметь привязку к разным сущностям. Сам я тоже использую в своих проектах комментарии к разнородным сущностям, поэтому хочу поделиться своей реализацией.

Давным давно в тридевя... в шаблонизаторе моей второй по счёту старой CMS вставка виджетов в шаблон была релизована посредством использования старой доброй клинописи вида {{WidgetName|param1=val1;param2=val2}}. Этот код можно было встявлять даже в текст, что давало неоценимую возможность компоновать страницы любой сложности из виджетов прямо в текстовом редакторе админки.

Комментарии

 

Максим

Здравствуйте !
Не могли бы Вы дать ссылку на упомянутое в самом начале решение с чекбоксами ?
Хотя бы для сравнения с Вашим, которое мне очень нравится, но все-таки хотелось бы иметь возможность групповых 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"))',

При этом возникает ошибка

include(CJavaScriptExpression.php): failed to open stream: No such file or directory

Что это может значить ?

Ответить

 

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

Здравствуйте. В вашем фреймворке не нашёлся класс CJavaScriptExpression. Он доступен с версии 1.1.11.

Ответить

 

Максим

Спасибо.
Обновил до текущей версии.
Работает

Ответить

 

Максим

Скажите пожалуйста, можно ли использовать DToggleColumn, если поле, с которым она связана, может принимать значения не только 0 и 1, но 2,3,4 и т.д. ?
0 трактуется как false, все остальные значения - true.

Ответить

 

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

Можно, так как там простое условие с такой же трактовкой

$value ? ' toggle-true' : ''
Ответить

 

Максим

А как в этом случае быть со свойствами filter и titles ?

Ответить

 

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

Будут нормально работать. Только значки и titles будут также отображать всего два состояния (Активно/Нет), а filter нормально работает сам по себе напрямую с провайдером данных.

Ответить

 

Максим

Увы, увы ...

Во время исполнения в строке 178 DToggleColumn.php возникает ошибка Undefined offset: 9

Ладно, с этим я справился, заменив $title = $this->titles[(int)$value]; на $title = $this->titles[($value)?1:0];

Но с фильтром проблема серьезнее

CDbCommand failed to execute the SQL statement: SQLSTATE[42S22]: Column not found: 1054 Unknown column 't.num' in 'where clause'.

Это, конечно, сугубо моя проблема, ибо поле 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 для грида. В самом виджете стоит

'dataProvider'=>$model->search()

$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

Ответить

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

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


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



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