Создаём свои типы ячеек для CGridView в Yii

CGridView

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

Представим, что записи блога на Yii содержат дату, изображение, заголовок и текст. Тогда стандартный грид в панели управления можно вывести так:

<?php $this->widget('zii.widgets.grid.СGridView', array(
    'dataProvider' => $model->search(),
    'filter' => $model,   
    'columns' => array(
        'date',
        'title',
        array(
            'class' => 'СButtonColumn',
        ),
    ),
)); ?>

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

Мы можем это сделать так:

<?php $this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider' => $model->search(),
    'filter' => $model,
    'columns' => array(
        array(
            'name' => 'image',
            'value' => '$data->image ? CHtml::link(CHtml::image("/upload/images/" . $data->image), Yii::app()->controller->createUrl("update", array("id" => $data->id))) : ""',
            'type' => 'html',
        ),
        array(
            'name' => 'date',
            'value' => 'CHtml::link(CHtml::encode($data->date), Yii::app()->controller->createUrl("update", array("id" => $data->id)))',
            'type' => 'html',
        ),
        array(
            'name' => 'title',
            'value' => 'CHtml::link(CHtml::encode($data->title), Yii::app()->controller->createUrl("update", array("id" => $data->id)))',
            'type' => 'html',
        ),
        array(
            'class' => 'СButtonColumn',
            'viewButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/view.png';
            'updateButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/edit.png';
            'deleteButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/del.png';
        ),
    ),
)); ?>

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

Но полёт фантазии этим иногда не ограничивается. Порой в сети можно встретить и такое:

'value'=> 'file_exists($_SERVER[DOCUMENT_ROOT] . 
    Yii::app()->urlManager->baseUrl . "/images/assortiment_img/thumb/" .
    $data->id . "_assortiment.jpg") ? Yii::app()->urlManager->baseUrl .
    "/images/assortiment_img/thumb_small/" . $data->id .
    "_assortiment.jpg" : Yii::app()->urlManager->baseUrl .
    "/images/assortiment_img/thumb_small/no_photo.gif"',

Мы собрали все ссылки в каждой ячейке явно. Копировать такие длинные и сложные строки из файла в файл неудобно. Попробуем это автоматизировать и упростить.

Стилизация колонок CGridView

Итак, у нас изображение, дата и заголовок статьи должны быть ссылками на действие редактирования. Чтобы не собирать каждый раз HTML-код со ссылкой нам необходимо сделать ячейки, которые бы делали это сами.

В нашем примере строка

'class' => 'СButtonColumn',

указывает, какой класс будет использоваться для генерации заголовка и ячеек столбца. Если класс не указан, то по умолчанию используется CDataColumn. Содержимое ячейки генерируется в его методе CDataColumn::renderDataCellContent. Следовательно, чтобы создать любой новый тип ячейки, можно переопределить данный метод в классе-наследнике и указывать этот класс в списке колонок.

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

Напишем первый класс на основе CDataColumn:

/**
 * @author ElisDN <mail@elisdn.ru>
 * @link http://www.elisdn.ru
 */
 
Yii::import('zii.widgets.grid.CDataColumn');
 
class DLinkColumn extends CDataColumn
{
    public $link;
 
    protected function renderDataCellContent($row, $data)
    {
        $url = $this->getItemUrl($row, $data);
        $value = $this->getItemValue($row, $data);
        $text = $this->grid->getFormatter()->format($value, $this->type);
        echo $value === null ? $this->grid->nullDisplay : CHtml::link($text, $url);
    }
 
    protected function getItemValue($row, $data)
    {
        if (!empty($this->value))
            return $this->evaluateExpression($this->value, array('data' => $data, 'row' => $row));
        elseif (!empty($this->name))
            return CHtml::value($data, $this->name);
        return null;
    }
 
    protected function getItemUrl($row, $data)
    {
        if (!empty($this->link))
            return $this->evaluateExpression($this->link, array('data' => $data, 'row' => $row));
        elseif ($this->link !== false)
            return Yii::app()->controller->createUrl('update', array('id' => $data->getPrimaryKey()));
        return '';
    }
}

Он автоматически оборачивает текст ячейки в ссылку.

Теперь вместо записи

array(
    'name' => 'title',
    'value' => 'CHtml::link(CHtml::encode($data->title), Yii::app()->controller->createUrl("update", array("id" => $data->id)))',
    'type' => 'html',
),

можно просто указать столбцу нужный класс:

array(
    'name' => 'title',
    'class' => 'DLinkColumn',
),

Этот столбец ввиду наследования поддерживает все атрибуты стандартного столбца CDataColumn, то есть можно также указывать value, header, type, htmlOptions и т.д.; использовать сортировку и фильтрацию.

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

Вот, например, подстановка ссылки на просмотр записи:

array(
    'name' => 'title',
    'class' => 'DLinkColumn',
    'link' => '$data->getUrl()',
),

Для этого в модели должен быть метод getUrl. И вообще, удобно добавлять метод getUrl ко всем моделям, у которых есть страница просмотра, и обращаться к ним по $model->url:

array(
    'name' => 'title',
    'class' => 'DLinkColumn',
    'link' => '$data->url',
),
array(
    'name' => 'author_id',
    'class' => 'DLinkColumn',
    'value'=> '$data->author->username',
    'link' => '$data->author->url',
),
array(
    'name' => 'category_id',
    'class' => 'DLinkColumn',
    'value'=> '$data->category->title',
    'link' => '$data->category->url',
),

Второй тип столбца, который нам нужен, аналогичен первому, но должен выводить изображение со ссылкой. Чтобы не повторять в нём метод getItemUrl возьмём его прямо из DLinkColumn:

/**
 * @author ElisDN <mail@elisdn.ru>
 * @link http://www.elisdn.ru
 */
 
class DImageLinkColumn extends DLinkColumn
{
    public $width = 0;
    public $height = 0;
 
    protected function renderDataCellContent($row,$data)
    {
        $url = $this->getItemUrl($row, $data);
        $value = $this->getItemValue($row, $data);
 
        $options = $this->getImageOptions();
        $image = CHtml::image($value, '', $options);
 
        echo $value===null ? $this->grid->nullDisplay : CHtml::link($image, $url);
    }
 
    protected function getImageOptions()
    {
        $options = array();
 
        if ($this->width)
            $options['width'] = $this->width;
        if ($this->height)
            $options['height'] = $this->height;
 
        return $options;
    }
}

Эта колонка теперь содержит дополнительные поля link, width и height.

Теперь вместо ручного генерирования ссылки

array(
    'name' => 'image',
    'value' => '$data->image ? CHtml::link(CHtml::image("/upload/images/" . $data->image), Yii::app()->controller->createUrl("update", array("id" => $data->id))) : ""',
    'type' => 'html',
),

мы аналогично указываем класс столбца и адрес изображения

array(
    'class' => 'DImageLinkColumn',
    'value' => '"/upload/images/blog/" . $data->image',
    'width' => 150,
),

Ещё приятнее создать метод getThumbUrl в каждой модели

class Post extends CActiveRecord
{
    const IMAGE_PATH = 'upload/images/blog';
    // ...
 
    private $_url;
 
    public function getUrl()
    {
        if ($this->_url === null)
            $this->_url = Yii::app()->createUrl('post/view', array('id'=>$this->id));
        return $this->_url;    
    }
 
    public function getImageUrl()
    {
        return $this->image ? $this->getImagePath() . '/' . $this->image : '';    
    }
 
    public function getThumbUrl()
    {
        return $this->image ? $this->getImagePath()  . '/prev_' . $this->image : '';    
    }
 
    protected funtion getImagePath()
    {
        return Yii::app()->request->baseUrl . '/' . self::IMAGE_PATH;
    }
}

и во всех гридах использовать его:

array(
    'class' => 'DImageLinkColumn',
    'value' => '$data->getThumbUrl()',
    'width' => 150,
),

Замена иконок кнопок в CButtonColumn

Следующий шаг – замена сложной записи по изменению изображений кнопок просмотра, редактирования и удаления

array(
    'class' => 'СButtonColumn',
    'viewButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/view.png';
    'updateButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/edit.png';
    'deleteButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/del.png';
),

на более простую и настраиваемую независимо форму.

Фактически, здесь мы видим использование класса СButtonColumn с указанием значений трёх его публичным полей, как и при задании опций виджетам. Но колонка грида – это не виджет, генерируемый с помощью фабрики widgetFacory, поэтому использовать скины для классов колонок мы не можем. Поэтому стоит воспользоваться наследованием с переопределением параметров исходного класса CButtonColumn:

Yii::import('zii.widgets.grid.CButtonColumn');
 
class DButtonColumn extends CButtonColumn
{
    public function init()
    {
        if ($this->viewButtonImageUrl === null)
            $this->viewButtonImageUrl = Yii::app()->request->baseUrl . '/images/admin/view.png';
        if ($this->updateButtonImageUrl === null)
            $this->updateButtonImageUrl = Yii::app()->request->baseUrl . '/images/admin/edit.png';
        if ($this->deleteButtonImageUrl === null)
            $this->deleteButtonImageUrl = Yii::app()->request->baseUrl . '/images/admin/del.png';
 
        parent::init();
    }
}

Перед инициализацией колонки мы проверяем эти параметры на заполнение, и если изображения не указаны пользователем, то подставляем свои.

С этим классом теперь вместо записи

array(
    'class' => 'СButtonColumn',
    'viewButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/view.png';
    'updateButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/edit.png';
    'deleteButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/del.png';
),

можно обойтись всего одной строкой

array(
    'class' => 'DButtonColumn',
),

В совокупности это упростит наш исходный фрагмент кода

<?php $this->widget('zii.widgets.grid.CGridView', array(
    'dataProvider' => $model->search(),
    'filter' => $model,
    'columns' => array(
        array(
            'name' => 'image',
            'value' => '$data->image ? CHtml::link(CHtml::image("/upload/images/" . $data->image), Yii::app()->controller->createUrl("update", array("id" => $data->id))) : ""',
            'type' => 'html',
        ),
        array(
            'name' => 'date',
            'value' => 'CHtml::link(CHtml::encode($data->date), Yii::app()->controller->createUrl("update", array("id" => $data->id)))',
            'type' => 'html',
        ),
        array(
            'name' => 'title',
            'value' => 'CHtml::link(CHtml::encode($data->title), Yii::app()->controller->createUrl("update", array("id" => $data->id)))',
            'type' => 'html',
        ),
        array(
            'class' => 'СButtonColumn',
            'viewButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/view.png';
            'updateButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/edit.png';
            'deleteButtonImageUrl' => Yii::app()->request->baseUrl . '/images/admin/del.png';
        ),
    ),
)); ?>

до состояния

<?php $this->widget('zii.widgets.grid.СGridView', array(
    'dataProvider' => $model->search(),
    'filter' => $model,
    'columns' => array(
        array(
            'class' => 'DImageLinkColumn',
            'value' => '$data->getThumbUrl()',
        ),
        array(
            'name'=>'date',
            'class' => 'DLinkColumn',
        ),
        array(
            'name' => 'title',
            'class' => 'DLinkColumn',
        ),
        array(
            'class' => 'DButtonColumn',
        ),
    ),
)); ?>

Аналогичный подход с наследованием позволяет нам сделать любой персональный тип колонок, например DToggleColumn или image-column.

Комментарии

 

Denis LED

В этом моменте случайно нет опечатки?

можно обойтись всего одной строкой
array(
    'class' => 'ButtonColumn', // точно не DButtonColumn?
),

ну и ниже
"до состояния"...

Ответить

 

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

Спасибо. Исправил.

Ответить

 

red-ogurez

У вас ошибка в

'valie'=> '$data->author->username', // должно наверное быть 'value'
'valie'=> '$data->category->title', // должно наверное быть 'value'
Ответить

 

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

Спасибо, исправил. Мимо клавиши промахнулся.

Ответить

 

Алексей

Прошу прощение за тупой вопрос, но:
"Напишем первый класс на основе CDataColumn:"
а где мы этот свой класс пишем?

Ответить

 

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

Одноимённым файлом в protected/components, например.

Ответить

 

Катя

Не могли бы Вы подсказать?

На главной странице через action admin выводится grid и поиск по нему. Хочу дать возможность пользователю дать выбрать доп.поля. Для этого создаю action ajax. Когда ввожу данные, находясь по урл article/ajax, пост приходит нормальным, а когда по article/admin в посте приходит еще html код страницы. Что я делаю не так?

Вот код:

public function actionAdmin()
{
	$model=new Article('search');
	$model->unsetAttributes();  

	if(isset($_GET['Article']))
		$model->attributes=$_GET['Article'];
		print_r($_POST);

	$this->render('admin',array(
		'model'=>$model,
		'db_name'=>'test',
		'table_name'=>'article',
	));
}

public function actionAjax()
{
	$model=new ArticleForm;

	if(isset($_POST['ArticleForm']))
	{
		$model->attributes=$_POST['ArticleForm'];
		if($model->validate())
		{
			print_r($_REQUEST);
			return;
		}
	}
	$this->render('article_form',array('model'=>$model));

}

Во view admin.php вывожу форму

<?php $this->renderPartial('article_form',array(
	'model'=>$model,
)); ?>

Форма article_form.php

<div class="form">

	<?php $form=$this->beginWidget('CActiveForm', array(
		'id'=>'article-form-form',
		'enableAjaxValidation'=>false,
		'htmlOptions'=>array(
			'onsubmit'=>"return false;",
			'onkeypress'=>" if(event.keyCode == 13){ send(); } "
		),
	)); ?>

	<?php echo $form->errorSummary($model); ?>

	<div class="row">
		<?php echo $form->labelEx($model,'id'); ?>
		<?php echo $form->textField($model,'id'); ?>
		<?php echo $form->error($model,'name'); ?>
	</div>
	<div class="row">
		<?php echo $form->labelEx($model,'name'); ?>
		<?php echo $form->textField($model,'name'); ?>
		<?php echo $form->error($model,'name'); ?>
	</div>


	<div class="row buttons">
		<?php echo CHtml::Button('SUBMIT',array('onclick'=>'send();')); ?>
	</div>

	<?php $this->endWidget(); ?>

</div><!-- form -->
<script type="text/javascript">

	function send()
	{

		var data=$("#article-form-form").serialize();


		$.ajax({
			type: 'POST',
			url: '<?php echo Yii::app()->createAbsoluteUrl("article/ajax"); ?>',
			data:data,
			success:function(data){
				alert(data);
			},
			error: function(data) { // if error occured
				alert("Error occured.please try again");
				alert(data);
			},

			dataType:'html'
		});

	}

</script>

Модель:

class ArticleForm extends CFormModel
{

	public $id;
	public $name;
	public $description;
	public $additional1;
	public $additional2;
	public $additional3;

	public function rules()
	{
		return array(

		);
	}

	public function attributeLabels()
	{
		return array(
			'id'=>'ID',
			'name'=>'Name',
			'description'=>'Description',
			'additional1'=>'Additional1',
			'additional1'=>'Additional2',
			'additional1'=>'Additional3',
		);
	}

}
Ответить

 

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

Добавьте условие:

if (Ypp::app()->request->isAjaxRequest)
    $this->renderPartial(...);
else
    $this->render(...);
Ответить

 

Катя

Спасибо, попробую так сделать

Ответить

 

Андрей

Здравствуйте,
такая проблема.
Если я изменяю класс столбца, то в хедере с фильтром инпут не помещается в div. И из-за этого инпут залезает на соседний столбец

Ответить

 

Андрей

пока решил вот так:

protected function renderFilterCellContent() {
    echo CHtml::openTag('div', array('class'=>'filter-container'));
    parent::renderFilterCellContent();
    echo CHtml::closeTag('div');
}
Ответить

 

Эльвира

А вы не пробовали просто поменять type с 'html' на 'image' или 'url' ;) Глядишь и кода меньше писать бы пришлось.

Ответить

 

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

Пробовал, но приходилось URL самому каждый раз сочинять и width картинкам костылями через стили подцеплять. И одновременно image+url вывести не получалось.

Ответить

 

dekameron
$AR->get{PropertyName}();


Это же стандартный геттер, зачем писать так сложно, если достаточно $AR->{propertyName} ?

Ответить

 

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

А где Вы это нашли?

Ответить

 

dekameron

yii\base\CComponent.php:107

public function __get($name){
	$getter='get'.$name;
	if(method_exists($this,$getter))
		return $this->$getter();
	...
Ответить

 

dekameron

Имелось ввиду, что вместо $data->getThumbUrl(), например, можно писать $data->thumbUrl

Ответить

 

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

Да, можно.

Ответить

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

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


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



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