Меню с иконками на основе CMenu в Yii

Меню

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

Значки для пунктов меню, например, удобно использовать в панели администрирования (как на рисунке к этой записи).

Стандартное использование виджета CMenu в представлении выглядит так:

<?php $this->widget('zii.widgets.CMenu', array(
    'items'=>array(
        array(
            'label'=>'Home', 
            'url'=>array('site/index')
        ),
        // ...
        array(
            'label'=>'Login', 
            'url'=>array('site/login'),
            'visible'=>Yii::app()->user->isGuest
        ),
    ),
)); ?>

Массив пунктов для items удобно генерировать в модели. В модель Category можно добавить метод getMenuList() (его, кстати, можно взять из поведения DCategoryBehavior) и выводить меню «Категории» простым использованием этого метода:

class Category extends CActiveRecord
{    
    // ...    
 
    public function getMenuList()
    {
        $items = array();  
 
        $models = $this->findAll(array('order'=>'name ASC');        
        foreach ($models as $model)
        {
            $items[] = array(
                'label'=>$model->name,
                'url'=>Yii::app()->createUrl('blog/category' array('id'=>$model->id)),      
            );
        }
 
        return $items;
    }
}
<?php $this->widget('zii.widgets.CMenu', array(
    'items'=>Category::model()->getMenuList();
)); ?>

Мы немного отвлеклись на основы. Рассмотрим способы помещения значков в пункты меню.

1. Ручное размещение иконки как фон элемента

Этот способ предполагает размещение иконки фоном в инлайновом стиле элемента, что генерирует код <li style="background-image:url(...)"> для каждого пункта. Он используется, например, здесь.

<?php $this->widget('zii.widgets.CMenu', array(
    'items'=>array(
        array(
            'label'=>'Home', 
            'url'=>array('site/index'),
            'linkOptions'=>array('style'=>'background-image:url(/icons/home.gif);')          
        ),
        // ...
    ),
)); ?>

2. Ручное размещение тега IMG в надписи

Этот способ подразумевает непосредственную вставку самим пользователем тега <img src="..." /> к надписи для передачи полю label. Для поддержки HTML кода в надписях нужно отключить экранирование HTML-сущностей указав 'encodeLabel'=>false:

<?php $this->widget('zii.widgets.CMenu', array(
    'encodeLabel'=>false,
    'items'=>array(
        array(
            'label'=>'<img src="/icons/home.gif" /> Home', 
            'url'=>array('site/index')         
        ),
        // ...
    ),
)); ?>

Так как кодирование отключено, то нам нужно в этом случае самим экранировать символы названия категории в методе getMenuList() вызывая CHtml::encode($model->name):

<?php $this->widget('zii.widgets.CMenu', array(
    'encodeLabel'=>false,
    'items'=>Category::model()->getMenuList();
)); ?>
class Category extends CActiveRecord
{
    protected $iconPath = 'images/icons',
 
    // ...    
 
    public function getMenuList()
    {
        $items = array();  
 
        $models = $this->findAll(array('order'=>'name ASC');        
        foreach ($models as $model)
        {
            $image = CHtml::image($model->getIconUrl());
            $items[] = array(
                'label'=>$image . ' ' . CHtml::encode($model->name),
                'url'=>Yii::app()->createUrl('blog/category' array('id'=>$model->id)),         
            );
        }
 
        return $items;
    }
 
    public function getIconUrl()
    {
        return Yii::app()->request->baseUrl . '/' . $this->iconPath . '/' . $this->icon;
    }    
}

Оценка решений

Оба рассморенных способа немного «костыльные», так как неуниверсальные и несемантические. Несемантические – так как заставяют нас заботиться о представлении пунктов и генерировать HTML/CSS код. Неуниверсальны и неполиморфны – потому что не позволяют прозрачно использовать один и тот же метод getMenuList() в разных по типу меню.

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

<?php $this->widget('zii.widgets.CMenu', array(
    'items'=>array(
        array(
            'label'=>'Home', 
            'url'=>array('site/index'),         
        ),
        array(
            'label'=>'Blog', 
            'url'=>array('blog/index'),
            'items'=>Category::model()->getMenuList();          
        ),
        array(
            'label'=>'Contacts', 
            'url'=>array('site/contacts'),         
        ),
        // ...
    ),
)); ?>

и одновременно как меню разделов с иконками в сайдбаре:

<?php $this->widget('zii.widgets.CMenu', array(
    'items'=>Category::model()->getMenuList();
)); ?>

Для каждого типа меню тогда придётся делать свой метод вроде getSimpleMenuList(), getIconMenuList() и т.п.

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

3. Семантическое решение

С семантической точки зрения более правильно генерацию HTML-разметки или CSS-стилей производить в самом виджете на основе переданных ему от модели данных. Для этого нужно создать свой модернизированный виджет меню для каждого конкретного случая. Отнаследуемся от стандартного класса меню и перепишем в своём метод генерации пункта:

Yii::import('zii.widgets.CMenu');
 
/**
 * @author ElisDN <mail@elisdn.ru>
 * @link http://www.elisdn.ru
 */
 
class DIconMenu extends CMenu
{
    public $iconsPath = '/';
 
    protected function renderMenuItem($item)
    {
        $icon = !empty($item['icon']) ? CHtml::image($this->iconsPath . $item['icon'], $item['label']) : '';
        $options = isset($item['linkOptions']) ? $item['linkOptions'] : array();
 
        if(isset($item['url']))
        {
            if ($this->linkLabelWrapper !== null)
                $label = '<' . $this->linkLabelWrapper . '>' . $item['label'] . '</' . $this->linkLabelWrapper . '>';
            else
                $label = $item['label'];
 
            return $icon . CHtml::link($label, $item['url'], $options);
        }
        else
            return $icon . CHtml::tag('span', $options, $item['label']);
    }
}

Здесь мы воспользовались вариантом добавления тега <img>. При желании можно подсмотреть реализацию варианта с background-image:url(...) в компоненте EIconizedMenu.

Теперь где это необходимо (например, в главном меню или в боковой колонке сайта) использовать это меню вместо стандартного:

<?php $this->widget('DIconMenu', array(
    'iconPath'=>Yii::app()->request->baseUrl . '/icons/',
    'items'=>array(
        array(
             'label'=>'Home',
             'url'=>array('site/index'),
             'icon'=>'nome.png'
        ),
    ),
)); ?>

Мы добавили новый параметр icon для каждого пункта и параметр iconPath для указания пути. Модернизированная модель Category теперь может выглядеть так

class Category extends CActiveRecord
{    
    public $iconsPath = '/';
 
    // ...    
 
    public function getMenuList()
    {
        $items = array();  
 
        $models = $this->findAll(array('order'=>'name ASC');        
        foreach ($models as $model)
        {
            $items[] = array(
                'label'=>$model->name,
                'icon'=>$model->getIconUrl(),         
                'url'=>$model->getUrl(),         
            );
        }
 
        return $items;
    }    
 
    public function getIconUrl()
    {
        return Yii::app()->request->baseUrl . '/' . $this->iconPath . '/' . $this->icon;
    } 
 
    private $_url;
    public function getUrl()
    {
        if ($this->_url === null)
            $this->_url = Yii::app()->createUrl('blog/category' array('id'=>$model->id));
        return $this->_url;
    }
}

То есть этот метод ничем не отличается от первоначального стандартного варианта, за исключением добавленного поля иконки. Один и тот же getMenuList() можно полиморфно использовать для DIconMenu, CMenu и любых других виджетов меню.

Этот виджет сформирует меню с иконками вида

<ul>
    <li><img src="/icons/home.png" alt="Home" /><a href="/">Home</a></li>
</ul>

Но вы можете при желании написать свой класс, который как в EIconizedMenu будет добавлять стили к пунктам вроде

<ul>
    <li><a href="/" style="background-image:url('/icons/home.png');" alt="Home" />Home</a></li>
</ul>

На этом всё. Не бойтесь модернизировать готовые чужие классы наследуясь от них и переопределяя некоторые их методы. Это намного удобнее, чем каждый раз придумывать что-то сверхъестественное или чем изменять системные файлы.

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

Одна из вечных тем, то и дело всплывающих в сети и касаемых Yii Framework – это спор относительно использования в своих проектах прямых SQL запросов посредством DAO с одной стороны против использования ActiveRecord с другой. Ведь при разрастании объёмов данных и связей между ними в высоконагруженных проектах многие разработчики переходят от удобной объектной модели ActiveRecord к низкоуровневой работе с прямыми SQL запросами и с простыми асcоциативными массивами. Посмотрим, как в некоторых случаях можно разогнать выборки ActiveRecord почти до скорости DAO.

Разговорились сегодня насчёт вывода списка чекбоксов в админке для выбора категорий к записи, то есть для связи MANY_MANY. Предоположим, что в нашем блоге есть записи и категории. Или товары в магазине и категории. При этом у каждой записи или у каждого товара можно выбрать несколько категорий. Как вывести этот список на странице редактирования статьи или товара?

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

При разработке интернет-магазинов или различных информеров для сайтов часто приходится реализовывать получение актуальных курсов валют. Не все разработчики знают, что достаточно удобно получать курсы на любую дату используя API сайта Центрального банка РФ.

Комментарии

 

Марат Долотов

Хорошая статья! Только вот у меня в моделе $this->createUrl не сработал, заменил его просто на array. Спасибо!

Ответить

 

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

Да, точно. Должно быть Yii::app()->createUrl(...) или array(...).

Ответить

 

Евгений

Отличная статья. Такие мелочи помогают сделать хороший продукт.

Ответить

 

Алексей

Спасибо Вам! Пишите еще про Yii = )

Ответить

 

klay

А вместо

<ul>
    <li>
        <a href="#">Item</a>
    </li>
    <li>
        <a href="#">Item</a>
        <ul>
             .......
        </ul>
    </li>
</ul>

можно использовать разметку с div? так чтобы вложенность тоже строилась с использованием div

Ответить

 

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

Только если сами переопределите методы renderMenu и renderMenuRecursive в своём наследнике класса CMenu. А какой смысл в div?

Ответить

 

klay

Я сделал вот так http://vpaste.net/ZcHTo
Ну и использую вот так:

<?php Yii::app()->controller->widget('XMenu', array(
    'items'              => $items,  // в классе виджета подготовил
    'activeCssClass'     => 'current',
    'itemTagName'        => 'div',
    'containerTagName'   => 'div',
    'submenuHtmlOptions' => array(
        'class' => 'navigation-list'
    )
)); ?>

Насколько это уродливо? :)

Ответить

 

klay

Смысла наверное особого нет, просто верстальщик так сверстал http://i.imgur.com/DLnH0fK.png

Ответить

 

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

Лучше наоборот div на ul заменить и CSS немного подправить.

Ответить

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

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


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



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