Динамическое добавление групп элементов в формах Zend Framework с использованием ZendX_JQuery

в 23:37, , рубрики: Zend Framework, метки:

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

Также, не секрет, что существует общий подход к решению данного вопроса — добавление групп элементов через субформы. Логика этого подхода проста — в шаблоне посредством яваскрипта на форму добавляются необходимые группы элементов, в обработчике формы подсчитывается кол-во пришедших групп элементов и, соответственно их количеству, добавляются субформы, затем уже вся форма с субформами валидируется.

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

Я предлагаю решить этот вопрос посредством создания отдельного элемента формы, реализующего данную функциональность.

image

Перейдем к практической реализации этого.

Создадим новый 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>

image

Как видно, элемент 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

Поделиться

* - обязательные к заполнению поля