Элемент Zend_Form для выбора изображения из песочницы

Здравствуйте. Без долгих вступлений, хочу показать как выглядит элемент, о создании которого я собираюсь рассказать:

Элемент Zend_Form RadioImage
Я решил назвать это RadioImage.

Недавно понадобилось предоставить пользователю возможность загрузки и выбора иконки для статусов продуктов в интернет-магазине. Раньше задачу выбора маленьких иконок я решал с помощью различных jQuery плагинов (эта картинка не моя):

Елемент Select с иконками
(и то, кстати очень ленился и не писал отдельный декоратор/элемент, а просто обходился JavaScript'ом).

Но в этот раз иконки могут быть разного размера и вообще мельчить не хочется, а если сделать select с большими картинками, то он будет выбиваться из общего вида формы.

Вспомнил, что я уже делал подобное и полез смотреть:
Чекбоксы с картинками
В принципе нормальный вариант, но я думаю, нормальный для нас — разработчиков. Для некоторых может быть непонятно, зачем тут нужен такой посредник как checkbox. Плюс ко всему, это опять же не элемент Zend_Form типа MultiImageCheckbox, а просто сгенерированный html прямо в скрипте вида — не хорошо. Если пригодилось во второй раз, нужно сделать по человечески.

На этот раз мне нужно предоставить выбор только одной иконки, поэтому наш элемент не позволит множественный выбор (но это не сложно поправить, создав другой элемент на основе этого, CheckImage например).

Еще есть одно требование — jQuery, я привык использовать хелпер вида ZendX_JQuery, но вы можете подключать файл с js кодом используя Zend_VIew_Helper_HeadScript.

Поехали, код с комментариями. Начну задом наперед — с формы:

application/forms/ProductStatus.php
<?php
class Form_ProductStatus extends Zend_Form
{
    public function init()
    {
        $this->setMethod('post');
        $this->setName('statusform');
        $this->setAttrib('enctype', 'multipart/form-data');
        
	$this->addElement('text', 'prodstatus_name', array(
	    'required'    => true,
	    'label'       => 'Status Name',
	    'filters'     => array('StringTrim')
	));
             
        // Инициализируем наш новый элемент
        $img = new App_Form_Element_RadioImage('prodstatus_icon', array(
            'label' => 'Select Icon',
            // HTML аттрибуты вроде этого будут применены к каждой картинке <img />
            'width' => '48'
        ));

        // BaseUrl думаю все знают он в основном для того,
          // если ваше приложение лежит НЕ в корне веб-сервера
        $bu = $this->getView()->baseUrl();

        // для простоты убрал сканирование директории/запрос к БД, 
          // покажу сразу каким должен быть массив
        $icons = array(
            "black_new.png"    => $bu.'/icons/black_new.png',
            "black_sale.png"   => $bu.'/icons/black_sale.png',
            "blue_new.png"     => $bu.'/icons/blue_new.png',
            "label_sale.png"   => $bu.'/icons/label_sale.png',
            "new_blue.png"     => $bu.'/icons/new_blue.png',
            "new_red.png"      => $bu.'/icons/new_red.png',
            "sale_blue.png"    => $bu.'/icons/sale_blue.png',
            "sale_green.png"   => $bu.'/icons/sale_green.png',
            "sale_yellow.png"  => $bu.'/icons/sale_yellow.png',
            "sticker_blue_sale.png" => $bu.'/icons/sticker_blue_sale.png'
        );

        // тут $key - значение элемента при отправке формы 
        // т. е. В контроллер придет [«prodstatus_icon»] => $key (если изображение выбрано)
        // $val это путь к картинке для тега img т.е. <img src="$val"/>
        foreach ($icons as $key => $val) {
            // и просто добавляем каждую картинку (так же как опции в элемент select)
              // можно конечно добавить используя addMultiOptions() без цикла 
              // но для наглядности я сделал цикл
            $img->addMultiOption($key, $val);
        }
        $this->addElement($img);

        $this->addElement('submit', 'Save');
    }
}


Может на первый взгляд выглядит как будто много кода, в живом проекте у меня это занимает несколько строк:
    $img = new App_Form_Element_SelectImage('prodstatus_icon', array(
        'label' => 'Select Icon',
        'width' => '48'
    ));
    $icons = App_Tool::scandir(PUBLIC_PATH.'/modules/products/icons', 'png');
    foreach ($icons as $icon) {
        $img->addMultiOption($icon, $this->getView()->baseUrl().'/modules/products/icons/'.$icon);
    }
    $this->addElement($img);


По-моему инициализация элемента достаточно проста — самое то, для повторного использования. Вот как это реализовано в библиотеке:

App/Form/Element/RadioImage.php
<?php
require_once 'Zend/Form/Element/Multi.php';

/**
 * RadioImage form element
 *
 * @category   App
 * @package    App_Form
 * @subpackage Element
 */
class App_Form_Element_RadioImage extends Zend_Form_Element_Multi
{
    /**
     * @var string
     */
    public $helper = 'FormRadioImage';
}


По аналогии с многими встроенными элементами Zend_Form, наш элемент является только интерфейсом к хелперу FormRadioImage, а вся логика в нем:

App/View/Helper/FormRadioImage.php
<?php
require_once 'Zend/View/Helper/FormElement.php';

/**
 * @category   App
 * @package    App_View
 * @subpackage Helper
 * @uses ZendX_Jquery
 */
class App_View_Helper_FormRadioImage extends Zend_View_Helper_FormElement
{
    /**
     * @param string|array $name Название элемента для параметра "name" тэга <input />
     * @param mixed $value Выбраное значение  по-умолчанию что бы 
     *                  пометить выбранное изображение.
     * @param array|string $attribs Html атрибуты для всех картинок.
     * @param array $options массив содержащий значение и путь для каждой картинки.
     * @return string конечный html
     */
    public function formRadioImage($name, $value = null, $attribs = null, $options = null)
    {

        $info = $this->_getInfo($name, $value, $attribs, $options);
        extract($info); // name, value, attribs, options

        // Убедимся что у нас именно массив (с путями и значениями для картинок)
        $options = (array) $options;

        $xhtml = '';
        $list  = array();
        // вот самый главные элемент несущий функционал, остальное интерфейс
        $list[]  = '<input type="hidden" id="'.$name.'" name="'.$name.'" value="'.$value.'" />';
        
        require_once 'Zend/Filter/Alnum.php';
        $filter = new Zend_Filter_Alnum();

        // Можно указать CSS класс для иллюстрации выбранности элемента
            // по умолчанию это class="selected"
        $selectedClass = (isset($attribs['selectedClass']) 
                && !empty ($attribs['selectedClass']))?$attribs['selectedClass']:'selected';
        $selectedClass = $filter->filter($selectedClass);
        if(!isset($attribs['class']))
            $attribs['class'] = null;
        // сохраняем указанные при инициализации элемента CSS классы (если указаны)
        $classBck = $attribs['class'];
        
        // начинаем добавлять изображения
        foreach ($options as $optVal => $imgPath) {

            // сгенерировать id для тэга <img />
            $imgId = $id . '-' . $filter->filter($optVal);

            // если выбран, добавляем к указанным класам еще и selected
            if ($optVal == $value) {
                $attribs['class'] .= " ".$selectedClass;
            }

            // сам код для картинки
            $list[] = '<img '
                    . 'src="'.$imgPath.'" '
                    . 'id="'.$imgId.'" '
                    . 'rel="'.$optVal.'" '
                    . $this->_htmlAttribs($attribs)
                    . '/>';
            
            // убрать класс selected, что бы остальные картинки не стали выбранными
            if(strstr($attribs['class'], $selectedClass))
                $attribs['class'] = $classBck;
        }
        // Добавить возможность отменить выбор, можно сделалть тут иконку вместо текста
        $list[]  = '<br /><a href=\"javascript;\">Reset Selection</a>'.PHP_EOL;
        $xhtml .= implode(PHP_EOL, $list);
        
        // подсветить выбранную картинку с jQuery
            // а так же подставить нужное значение в hidden элемент
        $this->view->jQuery()->addOnLoad("
            // клик по изображению
            $('#$name-element img').click(function(){
                $('#$name').val($(this).attr('rel'));
                $('#$name-element img').removeClass('$selectedClass');
                $(this).addClass('$selectedClass');
            });
            // кнопка отмены выбора
            $('#$name-element a').click(function(){
                $('#$name-element img').removeClass('$selectedClass');
                $('#$name').val('')
                return false;
            });
        ");
        // стиль для выбранного изображения (по-умолчанию)
            // можно указать другой css класс и описать его у себя в CSS
        $this->view->headStyle("
            #$name-element img {cursor:pointer; border:3px solid white}
            #$name-element img.selected {border:3px solid blue}
        ");

        return $xhtml;
    }
}


Все. Кажется ничего не забыл.

Картинка еще раз, что бы не скролить вверх:

Элемент Zend_Form RadioImage

Спасибо за внимание.
+22
22 сентября 2011, 13:14
35

комментарии (37)

+2
Kant #
Было бы круто увидеть демо.
+1
vladimir_e #
Да, я тоже об этом думал. Но у меня сейчас нет хабраэффекто-устойчивых мощностей.
0
vasfed #
Полагаю, что под отдачу нескольких десятков килобайт статики сильно много мощности не нужно (а сами jquery и тп можно и с гуглевского CDN заинклудить для пущей экономии трафика)
0
taliban #
Зенд на слабеньких машинах работает слабенько, особенно с формой, и внешними декораторами для элементов
0
dmitry_dvp #
Почему именно внешними?
0
taliban #
Я имел ввиду не в шаблоне описывать элементы а классы использовать, это сокращает скорость
0
anycolor #
не обязательно для демо тащить за собой весь зенд, это же только Zend_Form нужен + пару зависимых классов.
–2
JiLiZART #
Ах сколько же много кода ) Привет карме.
+2
vladimir_e #
Не поделитесь со мной, что это значит? Хм… тут должно быть, зашифрована конструктивная критика по теме поста.
0
JiLiZART #
Вся критика связана с архитектурой ZF 1.x и стилем написания кода для него.
+2
dmitry_dvp #
На самом деле в этой статье использован один из стилей работы.
Сами Zend'овцы всё больше склоняют пользователей фреймворка уходить от создания объектов, вызова методов в сторону к фабрикам и массивам характеристик создаваемого объекта (а в конечном счете — файлам конфигурации).

Например, Form_ProductStatus по-большому счёту лишняя сущность. Её бы могла успешно заменить секция в конфигурационом-файле (ini, xml, ...) с описанием характеристик формы.

Вот промежуточный этап рефакторинга в сторону увода формы этой в конфиг: pastebin.com/wMGM6xPZ
Как видите — остался единственный вызов метода, который тоже уйдет, когда форма будет доделана из статически наполненной опциями в динамически наполняемую данными перед выводом.
+1
vladimir_e #
Интересно, я не уделял должного внимания такому подходу. У меня не было аргументов «За» такой способ инициализации форм. Теперь задумался.
А мне .ini формат больше, чем массивы нравится (использую для маршрутов), нужно будет попробовать и посмотреть как пойдет с формами.
0
guyfawkes #
По идее, еще setValue нужно переопределить, например.
+1
dmitry_dvp #
Не припомню ни одного случая, чтобы приходилось переопределять setValue.
Уверен, что желание необоснованное
0
guyfawkes #
А если я хочу выделить какой-то элемент из списка изображений? Как в таком случае это будет работать?
0
dmitry_dvp #
также, как работает родной select, multicheckbox и radiogroup
+1
Anexroid #
Спасибо, думаю пригодится.

Единственное, я бы еще валидатор добавил.
+1
dmitry_dvp #
какой валидатор?
валидатор, проверяющий, что присланное значение — одно из перечисленных? Он встроен в Zend_Form_Element_Multi, который в данной статье экстендится
0
dmitry_dvp #
// вот самый главные элемент несущий функционал, остальное интерфейс
$list[] = '<input type=«hidden» id="'.$name.'" name="'.$name.'" value="'.$value.'" />';

1. конкатенации — жесть. почва для XSS. $view->escape() спасет
2. ID и NAME — не одно и тоже. В сабформу такой элемент не засунуть
3. вы же не зря экстендили Zend_View_Helper_FormElement… а у него есть метод $this->_hidden… к томе же есть хелпер $view->formHidden
0
dmitry_dvp #
4. не учтен $view->doctype()->isXhtml()
0
vladimir_e #
По первым трем пунктам согласен, спасибо. А doctype учел, но выкосил для статьи, что бы все выглядело максимально просто, передать основную суть элемента. Возможно был не прав — нельзя «учить» на неправильных примерах.
0
Alexznadr #
Хм. Складывается ощущение, что с данной работой вполне мог бы справиться Zend_Form_Element_Radio + css стайлинг от выделенного верстальщика. С другой стороны — далеко не везде ещё поняли необходимость выделенного верстальщика + если проект только один, для аутсорса этой работы нужно чтобы работа по проекту хоть как-то планировалась — а с этим у многих «какбэПМ»ов проблемы.
0
vladimir_e #
Ну можно было и так, но я в середине поста показал пример с чекбоксами — мне не нравится то, что эти элементы как посредники. Во времена драг-энд-дропов и тачскринов, хочется нажать непосредственно на элемент и что бы он выбрался. Я не профи в UI, но интуитивно так хочется сделать.
А как заменить радио кнопки, изображениями и сделать их «живыми» с помощью только стилей, я не представляю даже.
0
Alexznadr #
«с помощью только стилей» — почему только стилей — ещё и js есть. Но я просто о том, что работу верстальщика/client-side разработчика приходится выполнять server-side — разработчику.
+1
dmitry_dvp #
сканирование директорий, наполнение опциями. Всё это положено делать ВНЕ формы. Вы жестко завязали форму на предметную область убив один из принципов — повторное использование кода.

следовало по аналогии с multioptions наполнять через параметры к форме.

URL'ы тоже должны приходить в форму снаружи.

Уж коли @uses ZendX_Jquery, то написали бы простенький juqery плагинчик, в котором бы был завернут JS код, который обрабатывает клики, да и стили бы универсально вынесли. Конкатенация CSS селекторов опять же — не хорошо.

Ещё лучше — эксендить UiWidget, но тут есть камень преткновения — надо нелегкий jqueryui.wiget.core за собой таскать.

По HTML тоже можно было бы сделать лучше: чтобы работало и без javascript, сделать label, img и radio, а при инициализации JS radio прятать. Hidden вообще бы не потребовался
0
vladimir_e #
Полностью согласен. Понимаю, что это не оправдание, но я практически выдернул это из рабочего проекта (из админки) и захотел поделится.
В связи с недостатком времени, я делаю такие штуки итерациями. То есть, когда понадобится этот элемент еще раз, я поправлю такие жизненно важные вещи как использование конкатенации и возможность использовать без JS. Потом можно будет и обверткой заняться.

Без ваших камментов я не учел бы конечно всего. Так что это, получилось продолжение поста — обязательно к прочтению. Спасибо.
0
dmitry_dvp #
ну и такая вот мелочь: вью-хелпер должен или делать jquery->enable или проверять isEnabled
0
alxsad #
Можно просто

$img->addMultiOptions(array(
"black_new.png" => $bu.'/icons/black_new.png',
"black_sale.png" => $bu.'/icons/black_sale.png',
"blue_new.png" => $bu.'/icons/blue_new.png',
"label_sale.png" => $bu.'/icons/label_sale.png',
"new_blue.png" => $bu.'/icons/new_blue.png',
"new_red.png" => $bu.'/icons/new_red.png',
"sale_blue.png" => $bu.'/icons/sale_blue.png',
"sale_green.png" => $bu.'/icons/sale_green.png',
"sale_yellow.png" => $bu.'/icons/sale_yellow.png',
"sticker_blue_sale.png" => $bu.'/icons/sticker_blue_sale.png'
));
0
vladimir_e #
Да, спасибо. Я там в камментариях прямо в коде написал:
// можно конечно добавить используя addMultiOptions() без цикла 
// но для наглядности я сделал цикл
0
Alexznadr #
а можно оставить только названия классов и вынести в css.
0
alxsad #
это если с учетом того, что пункты будут не динамическими
0
Alexznadr #
да. вы правы.Но тут — что вижу, то и пою. Тут пункты статические и kiss говорит мне не гоородить лишний огород, да и програмного кода меньше в файле.
0
ferrari #
хм, всё смешалось — кони… люди… (ц)

почему это не список с радиокнопками, стилизованный через css, и инициализирующийся при помощи js?
и будет деградация для случая с отключенным/поломанным js у браузера и не будет зашитых путей картинок в php-код/конфиг/скрытую логику вроде вашей со сканированием папки?

да и появится возможность добавить например :hover версию картинки. в общем надо по-максимуму перетянуть всё в css, а в коде чтоб мелькали только id="..." и data-key="...." если нужно.
0
dmitry_dvp #
А если картинки — аватры пользователей, например
0
nalomenko #
Вторая картинка — создание нового Issue в до боли знакомой JIRA :)
0
Electronick #
Zend_Form наверное один из самых несуразных компонентов ZF, и то, как его используют — яркое подтверждение тому. И эта статья тоже.
0
Anexroid #
И что же именно в нём не так?

Я лично очень люблю ZF и в частности, Zend_Form за то, что он позволяет разворачивать очень большие формы с кучей всяких проверок и т.д за короткий срок. И не занимая место на элементы формы в template

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.