Подходы к стилизации виджетов в 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' => '&laquo; назад',
        'nextPageLabel' => 'далее &raquo;',    
    ), 
)); ?>

Здесь мы указали ему дополнительные опции, а именно добавили «ёлочки» к надписям ссылок.

Обычно стилизация на этом не останавливается, так как для переработки виджетов под персональный дизайн приходится менять шаблоны и прописывать свои CSS правила. Например, чтобы вывести два паджинатора над и под списком элементов (не используя CListView), нужно написать в представлении так:

<?php $this->widget('CLinkPager', array(
    'pages' => $pages,
    'header' => '',
    'prevPageLabel' => '&laquo; назад',
    'nextPageLabel' => 'далее &raquo;',
    'maxButtonCount' => 10,
    'cssFile' => Yii::app()->theme->baseUrl . '/css/pager.css',
    'htmlOptions' => array(
        'class' => 'paginator'
    ),     
)); ?>
 
<?php foreach ($items as $item): ?>
<!-- вывод элементов -->
<?php endforeach; ?>
 
<?php $this->widget('CLinkPager', array(
    'pages' => $pages,
    'header' => '',
    'prevPageLabel' => '&laquo; назад',
    'nextPageLabel' => 'далее &raquo;',
    '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}&ndash;{end} из {count}',
    'pager'=> array(        
        'header' => '',
        'prevPageLabel' => '&laquo; назад',
        'nextPageLabel' => 'далее &raquo;',
        '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 = '&laquo; назад';
    public $nextPageLabel = 'далее &raquo;';
    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,
)); ?>
 
<?php foreach ($items as $item): ?>
<!-- вывод элементов -->
<?php endforeach; ?>
 
<?php $this->widget('LinkPager', array(
    'pages'=>$pages,
)); ?>

И вместо огромного блока

<?php $this->widget('CLinkPager', array(
    'pages' => $pages,
    'header' => '',
    'prevPageLabel' => '&laquo; назад',
    'nextPageLabel' => 'далее &raquo;',
    '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}&ndash;{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}&ndash;{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' => '&laquo; назад',
        'nextPageLabel' => 'далее &raquo;',
        '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}&ndash;{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}&ndash;{end} из {count}',
    'pager'=> array(        
        'header' => '',
        'prevPageLabel' => '&laquo; назад',
        'nextPageLabel' => 'далее &raquo;',
        '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 классы и настройки всех паджинаторов, гридов, форм и любых других виджетов, изменив всего один файл прямо в текущей теме сайта.

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

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

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

Необходимость работы с громадными массивами в одном из проектов привела к альтернативному расширению для сериализации данных igbinary. Прочитать о нём кое-что можно здесь. Расширение добавляет функции igbinary_serialize и igbinary_unserialize, которые, в отличие от стандартных функций сериализации, конвертируют данные в бинарный блок. Если автор обзора не обманывает, то расширение даёт «5-кратное преимущество по размеру и 20-кратное по скорости».

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

Комментарии

 

Ra

А чем это лучше или хуже, чем widgetFactory, как в доках?

return array(
    'components'=>array(
        'widgetFactory'=>array(
            'widgets'=>array(
                'CLinkPager'=>array(
                    'maxButtonCount'=>5,
                    'cssFile'=>false,
                ),
                'CJuiDatePicker'=>array(
                    'language'=>'ru',
                ),
            ),
        ),
    ),
);
Ответить

 

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

Этот вариант упомянут в начале раздела «Темизация через скины».

Ответить

 

Sodu

Спасибо автору, нигде не нашел более адекватного блога о yii.

Ответить

 

Дмитрий

А файл views/skins/CGridView.php, как должен выглядеть внутри?
Что кроме этого там будет:

return array(
    'default' => array(
        'enableHistory' => false,
        'cssFile' => Yii::app()->request->baseUrl . '/core/css/gridview.css',
        'summaryText' => '{start}&ndash;{end} из {count}',
    ),
);
Ответить

 

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

Только это и будет.

Ответить

 

Алексей

Дмитрий, кажется, у вас лучшие материалы по Yii в Рунете. Спасибо!

Ответить

 

Алексей

А как "по-быстрому" задать CSS стиль, не прибегая к созданию отдельного .css файла и упоминания его в 'cssFile'?

Ответить

 

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

Вписать его в свой site.css.

Ответить

 

Алексей

Я имел в виду из кода yii. Хоть это и не правильно, но сделал через htmlOptions

Ответить

 

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

Во второй только так.

Ответить

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

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


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



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