Подходы к стилизации виджетов в Yii
В комплекте Yii Framework изначально поставляется набор виджетов Zii. Программисты знакомятся с обилием возможностей по стилизации виджетов, начинают кастомизировать, но не сразу доходят до удобных способов стилизации. В этом рецепте мы попробуем максимально сократить размеры копируемого из вида в вид кода.
Многие приведённые далее действия применимы к стандартным (CLinkPager
, CGridView
, CListView
, CDetailView
, CActiveForm
) и вашим собственным виджетам. Мы рассмотрим для примера только работу с гридом и паджинатором.
Оформление CGridView и CLinkPager
Итак, паджинатор можно стилизовать, используя его как специально отдельно:
$this->widget('CLinkPager', array( 'pages' => $pages, 'prevPageLabel' => '« назад', 'nextPageLabel' => 'далее »', ));
так и «не нарочно», так как он же используется в составе CGridView
, CListView
:
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'pager'=> array( 'prevPageLabel' => '« назад', 'nextPageLabel' => 'далее »', ), ));
Здесь мы указали ему дополнительные опции, а именно добавили «ёлочки» к надписям ссылок.
Обычно стилизация на этом не останавливается, так как для переработки виджетов под персональный дизайн приходится менять шаблоны и прописывать свои CSS правила. Например, чтобы вывести два паджинатора над и под списком элементов (не используя CListView
), нужно написать в представлении так:
<?php $this->widget('CLinkPager', array( 'pages' => $pages, 'header' => '', 'prevPageLabel' => '« назад', 'nextPageLabel' => 'далее »', 'maxButtonCount' => 10, 'cssFile' => Yii::app()->theme->baseUrl . '/css/pager.css', 'htmlOptions' => array( 'class' => 'paginator' ), )); foreach ($items as $item): <!-- вывод элементов --> endforeach; $this->widget('CLinkPager', array( 'pages' => $pages, 'header' => '', 'prevPageLabel' => '« назад', 'nextPageLabel' => 'далее »', 'maxButtonCount' => 10, 'cssFile' => Yii::app()->theme->baseUrl . '/css/pager.css', 'htmlOptions' => array( 'class' => 'paginator' ), ));
Класс CLinkPager
, конечно же, имеет и другие параметры, но мы их трогать не будем.
Перейдём теперь к CGridView
. Пусть стандартный грид для вывода списка записей блога выглядит так:
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'columns' => array( 'image', 'date', 'title', array( 'class' => 'СButtonColumn', ), ), ));
Когда мы укажем необходимые правила в коде вызова, это будет выглядеть монструозно:
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'enableHistory' => false, 'cssFile' => Yii::app()->theme->baseUrl . '/css/gridview.css', 'summaryText' => '{start}–{end} из {count}', 'pager'=> array( 'header' => '', 'prevPageLabel' => '« назад', 'nextPageLabel' => 'далее »', 'maxButtonCount' => 10, 'cssFile' => Yii::app()->theme->baseUrl . '/css/pager.css', 'htmlOptions' => array( 'class' => 'paginator' ), ), 'columns' => array( 'date', 'title', array( 'class' => 'СButtonColumn', ), ), ));
Здесь мы, всего лишь, переопределили некоторые шаблоны и указали виджетам свои файлы стилей.
В общем, каждый выводит гриды по своему, но это сейчас не так важно.
Проблема такого прямого подхода к стилизации в том, что при наличии нескольких десятков CRUD контроллеров появляется неудобство синхронизации. Все эти вызовы виджета нужно копировать в первозданном виде во все представления, и при изменении стиля править код в каждом.
Также у нас появится проблема при разработке второго сайта, так как там мы не сможем использовать эти же файлы представлений для другого дизайна. Конечно же, мы можем переопределить все представления в теме нового сайта, но это не очень экономный вариант. Так что попробуем найти альтернативы.
Переопределение параметров виджетов через наследование
CGridView и всё остальные компоненты фреймворка представляют из себя обычные классы с публичными полями, поэтому мы легко можем отнаследоваться от оригинала и переопределить значения этих полей по умолчанию.
Создадим класс LinkPager и перенесём передаваемые при вызове виджета значения внутрь него:
Yii::import('zii.widgets.grid.CLinkPager'); class LinkPager extends CLinkPager { public $header = ''; public $prevPageLabel = '« назад'; public $nextPageLabel = 'далее »'; public $htmlOptions = array( 'class'=>'paginator' ); public function __construct($owner=null) { $this->cssFile = Yii::app()->theme->baseUrl . '/css/pager.css'; parent::__construct($owner); } }
Здесь мы, например, берём файл стилей из текущей темы. Мы указали его в конструкторе, так как, в отличие от Java или ActionScript, язык PHP не позволяет использовать для инициализации полей вычисляемые выражения, то есть не разрешает написать вот так:
class LinkPager extends CLinkPager { // ... public $cssFile = Yii::app()->theme->baseUrl . '/css/pager.css'; }
Новый класс поместим, например, в protected/components
. Теперь вместо CLinkPager
можно использовать наш LinkPager
:
<?php $this->widget('LinkPager', array( 'pages'=>$pages, )); foreach ($items as $item): <!-- вывод элементов --> endforeach; $this->widget('LinkPager', array( 'pages'=>$pages, ));
И вместо огромного блока
<?php $this->widget('CLinkPager', array( 'pages' => $pages, 'header' => '', 'prevPageLabel' => '« назад', 'nextPageLabel' => 'далее »', 'maxButtonCount' => 10, 'cssFile' => Yii::app()->theme->baseUrl . '/css/pager.css', 'htmlOptions' => array( 'class' => 'paginator' ), ));
мы можем использовать всего одну строчку
<?php $this->widget('LinkPager', array('pages'=>$pagination));
Нашему гриду теперь нужно указать наш класс LinkPager
вместо стандартного:
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'enableHistory' => false, 'cssFile' => Yii::app()->theme->baseUrl . '/css/gridview.css', 'summaryText' => '{start}–{end} из {count}', 'pager'=> array( 'class' => 'LinkPager', ), 'columns'=>array(...), ));
Грид станет использовать наш паджинатор.
Теперь из грида аналогичным образом перенесём все параметры в класс GridView
:
Yii::import('zii.widgets.grid.CGridView'); class GridView extends CGridView { public $enableHistory = false; public $summaryText = '{start}–{end} из {count}'; public $pager= array( 'class' => 'LinkPager', ); public function __construct($owner=null) { $this->cssFile = Yii::app()->themer->baseUrl . '/css/gridview.css'; parent::__construct($owner); } }
и используем его:
<?php $this->widget('GridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'columns' => array( 'image', 'date', 'title', array( 'class' => 'СButtonColumn', ), ), ));
Таким образом, от стандартного паджинатора СLinkPager
<?php $this->widget('СLinkPager', array('pages'=>$pages));
мы перешли к полностью настраиваемому LinkPager
<?php $this->widget('LinkPager', array('pages'=>$pages));
а от стандартного грида CGridView
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'columns' => array(...), ));
к переделанному GridView
<?php $this->widget('GridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'columns' => array(...), ));
И всё это производится не дописыванием десятков опций в каждое представление, а простой сменой имени класса.
Теперь чтобы изменить стили всех виджетов для второго сайта нужно лишь изменить значение опций в классах
GridView
илиGridView
. Ни один файл представления менять не надо.
Темизация через скины
Переопределение стандартных классов – очень гибкое средство, но использовать его только для изменения стиля виджетов нерационально, так как фреймворк предоставляет для этого другую удобную возможность.
Описание можно найти в статье по темизации в официальном руководстве. Yii предоставляет для виджетов не только стандартное переопределение представлений, но и переопределение опций.
Первый подход подразумевает указание параметров в конфигурационном файле:
return array( 'components'=>array( // ... 'widgetFactory'=>array( 'widgets'=>array( 'CLinkPager'=>array( 'maxButtonCount'=>5, 'cssFile'=>'/themes/classic/css/pager.css', ), 'CJuiDatePicker'=>array( 'language'=>'ru', ), ), ), ), );
Но в этом случае мы должны вручную прописывать пути
'/themes/classic/css/pager.css'
и не можем использовать кострукции вроде
Yii::app()->themer->baseUrl . '/css/gridview.css'
так как конфигурационный файл считывается до запуска приложения. Также у нас может быть несколько тем у одного сайта, поэтому желательно уметь переопределять опции в темах.
Второй способ подразумевает использование так называемых скинов. Они представляют собой предопределённые группы параметров для определённого виджета.
Для работы данного функционала нужно включить поддержку скинов для компонента widgetFactory
:
return array( 'components'=>array( // ... 'widgetFactory' => array( 'enableSkin' => true, ), ), );
Теперь в папке views
приложения или темы нужно создать одноимённые виджетам файлы со скином по умолчанию default
:
views/skins/CLinkPager.php
return array( 'default' => array( 'header' => '', 'prevPageLabel' => '« назад', 'nextPageLabel' => 'далее »', 'cssFile' => Yii::app()->theme->baseUrl . '/pager.css', 'htmlOptions' => array('class'=>'paginator'), ), );
views/skins/CGridView.php
return array( 'default' => array( 'enableHistory' => false, 'cssFile' => Yii::app()->request->baseUrl . '/core/css/gridview.css', 'summaryText' => '{start}–{end} из {count}', ), );
Теперь наши стандартные виджеты
<?php $this->widget('CLinkPager', array('pages'=>$pages));
и
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'columns' => array(...), ));
будут выглядеть на разных сайтах так, как они описаны в скинах наших тем. Для разнообразия кроме default
можно сделать несколько скинов:
return array( 'default' => array(...), 'admin' => array(...), );
и при выводе виджета явно указывать используемый:
<?php $this->widget('CLinkPager', array('pages'=>$pages, 'skin'=>'admin'));
Таким образом, все стандартно сгенерированные в gii CRUD представления уже будут по умолчанию стилизованы под тему нашего сайта.
Результат
Какую же выгоду нам дали все эти преобразования? Достаточно сравнить фрагменты кода для вывода грида в представлениях.
Было:
<?php $this->widget('zii.widgets.grid.CGridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'enableHistory' => false, 'cssFile' => Yii::app()->theme->baseUrl . '/css/gridview.css', 'summaryText' => '{start}–{end} из {count}', 'pager'=> array( 'header' => '', 'prevPageLabel' => '« назад', 'nextPageLabel' => 'далее »', 'maxButtonCount' => 10, 'cssFile' => Yii::app()->theme->baseUrl . '/css/pager.css', 'htmlOptions' => array( 'class' => 'paginator' ), ), 'columns' => array( 'date', 'title', array( 'class' => 'СButtonColumn', ), ), ));
Стало:
<?php $this->widget('zii.widgets.grid.СGridView', array( 'dataProvider' => $model->search(), 'filter' => $model, 'columns' => array( 'date', 'title', array( 'class' => 'СButtonColumn', ), ), ));
То есть мы вернулись к минимальной стандартной записи, но при этом ко всем виджетам будут применяться наши стили.
Больше не надо указывать и копировать списки опций из представления в представление. Теперь очень легко, не влезая в код модулей, поменять CSS классы и настройки всех паджинаторов, гридов, форм и любых других виджетов, изменив всего один файл прямо в текущей теме сайта.
А чем это лучше или хуже, чем widgetFactory, как в доках?
Этот вариант упомянут в начале раздела «Темизация через скины».
Спасибо автору, нигде не нашел более адекватного блога о yii.
А файл views/skins/CGridView.php, как должен выглядеть внутри?
Что кроме этого там будет:
Только это и будет.
Дмитрий, кажется, у вас лучшие материалы по Yii в Рунете. Спасибо!
А как "по-быстрому" задать CSS стиль, не прибегая к созданию отдельного .css файла и упоминания его в 'cssFile'?
Вписать его в свой site.css.
Я имел в виду из кода yii. Хоть это и не правильно, но сделал через htmlOptions
Во второй версии Yii есть что-то аналогичное скинам? А то я ничего более подходящего чем вот это http://www.yiiframework.com/doc-2.0/guide-concept-di-container.html#practical-usage не нашёл.
Во второй только так.