Не секрет, что часто возникает необходимость добавить на форму элементы или группы элементов, количество которых может быть неопределенным или достаточно большим, чтобы указать их явно в конфигурации формы.
Также, не секрет, что существует общий подход к решению данного вопроса — добавление групп элементов через субформы. Логика этого подхода проста — в шаблоне посредством яваскрипта на форму добавляются необходимые группы элементов, в обработчике формы подсчитывается кол-во пришедших групп элементов и, соответственно их количеству, добавляются субформы, затем уже вся форма с субформами валидируется.
Для меня минус этого подхода заключается в том, что практически невозможно вынести конфигурацию формы в отдельное место (в отдельный файл конфигурации) и ее приходится «доконфигурировать» в обработчике формы.
Я предлагаю решить этот вопрос посредством создания отдельного элемента формы, реализующего данную функциональность.
Перейдем к практической реализации этого.
Создадим новый ZF проект
% zf create project www.multielement.lo
Инициализируем объект View с поддержкой JQuery в конфигурационном файле application.ini, устанавливаем путь к нашим помощникам, устанавливаем версии и пути к javascript-библиотекам
resources.view[] = ""
resources.view.helperPath.ZendX_JQuery_View_Helper = "ZendX/JQuery/View/Helper"
resources.view.helperPath.My_JQuery_View_Helper = "My/JQuery/View/Helper"
resources.jquery.version = "1.7"
Создадим FormController в /application/controllers/
<?php
class FormController extends Zend_Controller_Action
{
public function indexAction()
{
$opts = array(
'elements' => array(
'firstname' => array(
'type' => 'Text',
'options' => array(
'label' => 'Имя'
)
),
'lastname' => array(
'type' => 'Text',
'options' => array(
'label' => 'Фамилия'
)
),
'items' => array(
'type' => 'MultiElement',
'options' => array(
'label' => 'Товары',
'required' => true,
'elements' => array(
'name' => array(
'type' => 'Text',
'options' => array(
'label' => 'Наименование',
'required' => true
)
),
'type' => array(
'type' => 'Select',
'options' => array(
'label' => 'Цвет',
'required' => true,
'multioptions' => array(
'green' => 'Зеленый',
'red' => 'Красный',
'blue' => 'Синий',
)
)
),
'price' => array(
'type' => 'Text',
'options' => array(
'label' => 'Стоимость, руб.',
'required' => true
)
),
)
)
),
'logons' => array(
'type' => 'MultiElement',
'options' => array(
'label' => 'Явки и пароли',
'required' => true,
'elements' => array(
'login' => array(
'type' => 'Text',
'options' => array(
'label' => 'Логин',
'required' => true
)
),
'passw' => array(
'type' => 'Text',
'options' => array(
'label' => 'Пароль',
'required' => true
)
),
'type' => array(
'type' => 'Select',
'options' => array(
'label' => 'Социальная сеть',
'required' => true,
'multioptions' => array(
'vk' => 'Вконтакте',
'fc' => 'FaceBook',
'tw' => 'Twitter',
)
)
),
)
)
),
'submit' => array(
'type' => 'Submit',
'options' => array(
'label' => 'Отправить'
)
),
),
);
$form = new Zend_Form();
$form->addPrefixPath('My_JQuery_Form','My/JQuery/Form');
$form->setOptions($opts);
if($this->getRequest()->isPost()) {
if($form->isValid($this->getRequest()->getPost())) {
$values = $form->getValues();
$this->view->assign('MyFormValues',$values);
}
}
$this->view->assign('MyForm',$form->render());
}
}
Создадим скрипт вида в /views/scripts/form/index.phtml
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Пример формы с мультиэлементами</title>
<?php print $this->JQuery(); ?>
</head>
<body class="ui-widget">
<h1>Пример формы с мультиэлементами</h1>
<?php print $this->MyForm; ?>
<?php if($this->MyFormValues) { ?>
<pre>
<?php print_r($this->MyFormValues); ?>
</pre>
<?php } ?>
</body>
</html>
Как видно, элемент MultiElement содержит секцию опций «elements», которая совместима с Zend_Form_Element.
Принцип работы элемента MultiElement заключается в следующем:
- Для каждой группы элементов создается форма Zend_Form без декораторов Form и DtDdWrapper
- Каждому элементу в группе посредством setElementsBelongTo устанавливается имя вида элемент[][элемент_группы]
- Форма рендерится и кладется в опции элемента MultiElement для дальнейшего использования в помощнике вида
- Объект формы кладется в приватное свойство для дальнейшего использования его клонированных экземпляров в методах isValid и setValue
- В помощнике вида отрендеренная форма группы кладется в js-переменную и добавляется в объект View посредством addJavascript
- В помощнике вида создается кнопка «Добавить» и js для обработки кликов по ней
- При клике происходит вставка отрендеренной формы из js-переменной с заменой имен элементов на элемент[счетчик][элемент_группы]
- В помощнике вида также отрисовываются прищедшие значения групп элементов и сами группы
<?php
require_once "Zend/Form/Element/Xhtml.php";
class My_JQuery_Form_Element_MultiElement extends Zend_Form_Element_Xhtml {
public $helper = "multiElement";
/**
* Массив объектов формы для каждой группы дополнительных элементов
* @var array
*/
protected $forms = array();
/**
* Объект формы дополнительных элементов
* @var Phorm_Form
*/
protected $form;
/**
* Отрендеренная форма дополнительных элементов
* @var string
*/
protected $renderform = '';
/**
* Определяем массив опций и дергаем родительский конструктор
*
* @param mixed $spec
* @param array $options
*/
public function __construct($spec, $options = null) {
/**
* Выделяем дополнительные элементы в отдельную форму и рендерим ее
*/
if(isset($options['elements']) && is_array($options['elements'])) {
$form = new Zend_Form(array('elements'=>$options['elements']));
$form -> removeDecorator('Form');
$form -> removeDecorator('DtDdWrapper');
$form -> setElementsBelongTo($spec.'[]');
$this->renderform = $form->render();
unset($options['elements']);
$this->form = $form;
}
/**
* Инициализируем родительский конструктор
*/
parent::__construct($spec, $options);
}
/**
* Валидация элемента
*
* @param mixed $value
* @return boolean
*/
public function isValid($value) {
$this->_messages = array();
$this->_errors = array();
$this->setValue($value);
$value = $this->getValue();
if(!is_array($value) && $this->isRequired()) {
$this->_messages = array('Значение обязательно для заполнения и не может быть пустым');
return false;
}
$result = true;
if(is_array($value)) {
foreach ($value as $key=>$mini_form) {
if(key_exists($key,$this->forms)) {
$form = $this->forms[$key];
if(!$form->isValid($mini_form)) $result = false;
}
}
}
return $result;
}
/**
* Set element value
*
* @param array $value
* @return Zend_Form_Element
*/
public function setValue($value) {
if(!is_array($value)) return $this;
$this->_value = $value;
foreach ($value as $mini_form) {
$form = clone $this->form;
$this->forms[] = $form->setDefaults($mini_form);
}
return $this;
}
}
Как вы можете видеть, поддерживается опция required. При необходимости, элемент MultiElement можно расширить, добавив обработку дополнительных опций, фильтров и валидаторов
Код помощника вида
<?php
require_once "ZendX/JQuery/View/Helper/UiWidget.php";
class My_JQuery_View_Helper_MultiElement extends ZendX_JQuery_View_Helper_UiWidget {
/**
* Рисуем элемент
*
* @param string $id Id HTML-элемента
* @param string $value Значение элемента
* @param array $params Массив конфигурации из секции options
* @return string
*/
public function multiElement($id, $value = null, array $params = array()) {
/**
* Определяем форму с элементами группы
* Добавляем форму в JS
*/
$js_var = $id . '_subform';
if(isset($params['renderform'])) {
$this->jquery->addJavascript('var ' . $js_var . ' = '
. ZendX_JQuery::encodeJson($params['renderform']) . ';');
}
/**
* Определяем кнопку удаления группы
*/
$icon_delete = $this->view->formButton($id . '_delete', 'Удалить');;
/**
* Формируем кнопку добавления новой группы и вешаем на нее JS
*/
$button_id = $id . '_add';
$button = $this->view->formButton($button_id, 'Добавить');
$jquery_handler = ZendX_JQuery_View_Helper_JQuery::getJQueryHandler();
$js = array();
$js[] = sprintf('%s("#%s").next("ul").find("> li").prepend(%s("%s").click(function(){
if(confirm("%s")) %s(this).parent("li").remove();
return false;
}));',
$jquery_handler, $button_id, $jquery_handler, addslashes($icon_delete), 'Удалить?', $jquery_handler);
$js[] = sprintf('%s("#%s").click(function(){
var itr = %s(this).next("ul").find("> li").length-1;
var Tmpl = %s.replace(/name="%s[][/g,"name="%s["+itr+"][");
var li = %s(this).next("ul").find("li:last").clone(true).insertBefore(%s(this)
.next("ul").find("li:last")).append(Tmpl).show();
});',
$jquery_handler, $button_id, $jquery_handler, $js_var, $id, $id, $jquery_handler, $jquery_handler);
$this->jquery->addOnLoad(join(PHP_EOL,$js));
/**
* Отрисовываем переданные группы и шаблон
*/
$xhtml = array();
$xhtml[] = '<ul>';
$attribs = array();
foreach ($params as $k=>$v) if(in_array($k,array('class','style'))) $attribs[$k] = $v;
/**
* Устанавливаем пришедшие значения
*/
foreach ($params['forms'] as $key=>$form) {
$form -> setElementsBelongTo($id . '['.$key.']');
$xhtml[] = '<li' . $this->_htmlAttribs($attribs) . '>' . $form->render() . '</li>';
}
/**
* Отрисовываем "хвост" списка
*/
if(isset($attribs['style'])) $attribs['style'] .= ';display:none'; else $attribs['style'] = 'display:none';
$xhtml[] = '<li' . $this->_htmlAttribs($attribs) . '></li>';
$xhtml[] = '</ul>';
return $button . join(PHP_EOL,$xhtml);
}
}
Значения формы получаются в аккуратном иерархическом виде
[firstname] => Вася
[lastname] => Иванов
[items] => Array
(
[2] => Array
(
[name] => Бабочки
[type] => red
[price] => 1000
)
[3] => Array
(
[name] => Бревна
[type] => blue
[price] => 2000
)
)
[logons] => Array
(
[0] => Array
(
[login] => username
[passw] => qwerty
[type] => vk
)
)
К сожалению, в группу элементов нельзя добавить элемент File, поскольку он не поддерживает BelongTo. Я пробовал добавить ее поддержку в декораторе File, но столкнулся с проблемами в Zend_File_Transfer_Adapter, связанными с отсутствием обработки многомерных массивов в переменной $_FILES.
Можно попробовать формировать префикс в setBelongTo без [] и обрабатывать массив файлов отдельно от группы элементов, в которую они входят либо использовать какой-нибудь Ajax File Uploader вместо элемента File.
Скачать полностью рабочий пример можно отсюда
Автор: vkachalov