Zend Framework 2/3 набор сбора пользовательских атрибутов
Судя по этим вопросам:
Zend framework 2 просматривает элементы коллекции элементов
Я полагаю, что нет хорошего способа настроить элементы коллекции.
Как пример, имея такую коллекцию:
//path: MyModule\Form\MyFieldset
public function __construct($name = null) {
parent::__construct('myFieldset');
$this->add([
'name'=>'test',
'type' => Element\Collection::class,
'options' => [
'label' => 'MyCollection',
'count' => 6,
'should_create_template' => true,
'target_element' => new Element\Text()
],
]);
}
Затем сделайте что-нибудь, чтобы определить (здесь, в текущем классе) настраиваемые атрибуты для каждого текстового элемента и / или метки с автонумерацией и затем вывести (просто вызывая помощник Zend FormCollection без какого-либо настраиваемого помощника вида):
<label>
<span>text element n° 1</span>
<input type="text" name="myFielset[test][0]" id='myId_0' alt='input 0' value="">
</label>
<label>
<span>text element n° 2</span>
<input type="text" name="myFielset[test][1]" id='myId_1' alt='input 1' value="">
</label>
[...]
Я ошибся?
(Я спрашиваю об этом, потому что я нашел хорошее решение, чтобы сделать это, и, возможно, может быть полезно опубликовать его)
3 ответа
Решение, которое я нашел, имеет что-то общее с решением Ричарда Парнаби-Кинга:
target_element должен ссылаться на набор полей.
Но вместо того, чтобы установить счетчик клонов, он расширяет Zend\Form\Fieldset method prepareElement
Основное применение:
namespace Module\Form;
use Zend\Form\Fieldset;
use Zend\Form\FormInterface; //needed in order to call prepareElement method
class MyFieldset extends Fieldset {
public function __construct($name = null) {
parent::__construct($name);
$this->add([
'name' => 'question',
'type' => 'text',
'attributes' => [
'alt' => 'input',
],
'options' => [
'label' => 'Text',
],
]);
}//construct
public function prepareElement(FormInterface $form){
parent::prepareElement($form);
$name = $this->getName();
//Do stuff related to this fieldset
foreach ($this->iterator as $elementOrFieldset) {
$elementName=$elementOrFieldset->getName()
//Do stuff related to this fieldset children
}
}//prepareElement
}
Особенности:
- Автонумерация меток и / или атрибутов
- Позволяет условное присвоение атрибутов / меток
- Позволяет взаимодействие между различными элементами (например: передача идентификатора элемента A в элемент B в качестве цели)
- Работает с шаблонами
Поскольку это решение может быть разработано разными способами, я подготовил полную демонстрацию, готовую к запуску и изучению.
Примечание: эта демонстрация не лучшая реализация, а набор примеров, которые приводят к результату. :-)
Этот пример предназначен для запуска в модуле "Приложение" по умолчанию с использованием префикса "Bob", чтобы избежать конфликтов с другими файлами (я представлял, что у кого-то уже может быть файл с именем TestController, но, полагаю, ни у кого нет файла с именем BobController).
Тогда, если вы точно выполните следующие шаги, вы сможете без проблем запустить и исследовать демонстрацию.
Реализация prepareElement
метод в BobFieldset
класс может показаться массивным, но это просто вопрос комментариев, пробелов и примеров. Это может быть очень мало в зависимости от ваших потребностей.
ШАГ 1:
РЕДАКТИРОВАТЬ файл: Application \ config \ module.config.php
//add bob route to router
'router' => [
'routes' => [
'bob' => [
'type' => Literal::class,
'options' => [
'route' => '/bob',
'defaults' => [
'controller' => Controller\BobController::class,
'action' => 'index',
],
],
],
[...]
//add BobController
'controllers' => [
'factories' => [
[...]
Controller\BobController::class => InvokableFactory::class,
],
],
ШАГ 2:
СОЗДАТЬ файл: Application\src\ Controller \BobController.php
<?php
namespace Application\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Application\Form\BobForm;
class BobController extends AbstractActionController
{
public function __construct(){}
public function indexAction()
{
$form = new BobForm('album');
$request = $this->getRequest();
if( $request->isPost()){
$form->setInputFilter($form->getInputFilter());
$form->setData($request->getPost());
if (! $form->isValid()) {
return ['form' => $form];
}
}
return ['form' => $form];
}
}
ШАГ 3:
СОЗДАТЬ файл: Application\src\Form\BobForm.php
<?php
namespace Application\Form;
use Zend\Form\Element;
use Zend\Form\Form;
class BobForm extends Form
{
private $inputFilter;
public function __construct($name = null)
{
parent::__construct($name);
$this->setLabel('formBaseFieldset');
$this->add([
'name' => 'select',
'type' => 'select',
'options' => [
'label' => 'random element',
'value_options' => [
0 => null,
1 => 'someThing',
2 => 'someThingElse',
],
],
'attributes' => [
'value' => 0,
],
]);
$this->add([
'name' => 'answer',
'type' => Element\Collection::class,
'options' => [
'label'=>'bobFieldset',
'count' =>3,
'should_create_template' => true,
'target_element' => new \Application\Form\BobFieldset ,
],
'attributes' => [
'id'=>'bob',
],
]);
$this->add(array(
'name' => 'addNewRow',
'type' => 'button',
'options' => array(
'label' => 'Add a new Row',
),
'attributes' => [
'onclick'=>'return add_category()',
]
));
}//construct
public function getInputFilterSpecification() {
return array(
'select' => [
'validators' => [
['name' => 'NotEmpty'],
],
],
);
}
}
ШАГ 4:
СОЗДАТЬ файл: Application\src\Form\BobFieldset.php
<?php
namespace Application\Form;
use Zend\Form\Fieldset;
use Zend\Form\FormInterface; //needed in order to call prepareElement method
use Zend\InputFilter\InputFilterProviderInterface;
class BobFieldset extends Fieldset implements InputFilterProviderInterface
{
private $inputFilter;
public function __construct($name = null) {
parent::__construct($name);
$this->setLabel('bobFieldset: Answer __num__');
$this->add(array(
'name' => 'text',
'type' => 'text',
'options' => array(
'label' => 'Text __num__',
),
'attributes' => [
'customAttribute'=>' -> ', //see below
]
));
$this->add(array(
'name' => 'optionsButton',
'type' => 'button',
'options' => array(
'label' => 'Options',
),
'attributes' => [
'data-dialog-target'=>'options', //sub fieldset name
'class'=>'options',
]
));
$this->add( new \Application\Form\BobSubFieldset('options'));
}
public function prepareElement(FormInterface $form)
{
/*--->here we're looping throug the collection target_element(instance of BobFieldset)<---*/
//Leave untouched the default naming strategy
parent::prepareElement($form);
//output: (string) 'answer[$i]' -> BobFieldset's clone name attribute
//Note: $i corresponds to the instance number
$name = $this->getName(); //var_dump($name);
//output: array(0=>'answer',1=>$i)
$sections = $this->splitArrayString($name); //var_dump($sections);
//output: (string) $i ->When the collection's option 'should_create_template' is setted to true, the last one will be: (string) '__index__'
$key=end($sections); //var_dump($key);
//output (string) 'answer_$i' -> I guess this could be the easyest way to handle ids (easy to manipulate, see below)
$string=implode('_',$sections); //var_dump($string);
//Just because a label like 'answer number 0' is ugly ;-)
$keyPlus=(is_numeric($key)) ? $key+1 : $key; //var_dump($keyPlus);
//Since we're using different placeholders:
//Predefined __index__: used for names ($key)
//__num__: used for labels ($keyplus)
//Then we need this control to avoid replacements between placeholders(check below)
$isTemplate=($keyPlus==$key) ? true : false;
if(!$isTemplate){
//get the label of the current element(BobFieldset clone) and replace the placeholder __num__ (defined above) with the current key (+1)
$label = str_replace('__num__',($keyPlus),$this->getLabel());
$this->setLabel($label); //var_dump($this->getLabel());
}
/*--->From here we're looping throug the target_element (BobFieldset) children<---*/
foreach ($this->iterator as $elementOrFieldset) {
//output: (string) 'answer[$i][elementName]'
//answer[0][text]
//answer[0][optionsButton]
//answer[0][options]
//answer[1][text]
//...
$elementName=$elementOrFieldset->getName();//var_dump($elementName);
//Example: get specific element and OVERWRITE an autonumbered label
$sections = $this->splitArrayString($elementName);
$trueName=end($sections);
if($trueName=='text' && !$isTemplate){
$elementOrFieldset->setLabel('Input '.$keyPlus);
}
//Example2: get specific element via custom attribute
//Note: when an attribute isn't listed into the Zend\Form\View\Helper\AbstractHelper's $validGlobalAttributes (array) it will be automatically removed on render
//global attributes data-? will be rendered
if($target=$elementOrFieldset->getAttribute('customAttribute')){
$label=$elementOrFieldset->getLabel();
$elementOrFieldset->setLabel($label.$target);
}
//Reference another element as target for a javascript function
//button 'optionsButton' will have an attribute containing the id of the relative element 'options' (BobSubFieldset)
//Alternatives:
//1) work only with javascript & DOM
//2) set a javascript call directly: $elementOrFieldset->setAttribute('onclick','return doSomething();'); check BobForm 'addNewRow' button
if($target=$elementOrFieldset->getAttribute('data-dialog-target')){
$elementOrFieldset->setAttribute('data-dialog-target',$string.'_'.$target);
}
//set id for jqueryui dialog function. This id corresponds to the target setted above
//The attribute data-transform will be used as jquery selector to create the dialogs
if($elementOrFieldset->getAttribute('data-transform')=='dialog'){
$id = str_replace(['[', ']'],['_', ''],$elementName);
$elementOrFieldset->setAttribute('id',$id);
//Set and autonumbering the dialog title
if(!$isTemplate){
$title = str_replace('__num__',($keyPlus),$elementOrFieldset->getAttribute('title'));
$elementOrFieldset->setAttribute('title',$title);
}
}
}//foreach
}
public function splitArrayString($string){
return preg_split('/\h*[][]/', $string, -1, PREG_SPLIT_NO_EMPTY);
}
public function getInputFilterSpecification() {
return array(
'text' => [
'validators' => [
['name' => 'NotEmpty'],
],
],
);
}
}
ШАГ 5:
СОЗДАТЬ файл: Application\src\Form\BobSubFieldset.php
<?php
namespace Application\Form;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
class BobSubFieldset extends Fieldset implements
InputFilterProviderInterface {
private $inputFilter;
public function __construct($name = null) {
parent::__construct($name);
$this->setLabel('bobSubFieldset');
$this->setattribute('data-transform','dialog');
$this->setattribute('title','Options for answer __num__');
$this->add(array(
'name' => 'first',
'type' => 'text',
'options' => array(
'label' => 'firstOption',
),
'attributes' => [
]
));
$this->add(array(
'name' => 'second',
'type' => 'text',
'options' => array(
'label' => 'secondOption',
),
'attributes' => [
]
));
$this->add(array(
'name' => 'third',
'type' => 'text',
'options' => array(
'label' => 'thirdOption',
),
'attributes' => [
]
));
}
public function getInputFilterSpecification() {
return array();
}
}
ШАГ 6 (последний):
СОЗДАТЬ файл: Application\view\application\bob\index.phtml
Примечание: здесь я добавил все внешние js/css, которые я использовал, возможно, они уже есть в вашем макете.
<?php
$script=
"$(document).ready(function(){
$( '#bobContainer' ).on('click','button.options', function () {
//Retrieve the target setted before...
id=$(this).attr('data-dialog-target');
$('#'+id).dialog('open');
return false;
});
//We need a custo event in order to detect html changes when a new element is added dynamically
$( '#bobContainer' ).on( 'loadContent', function() {
//We need this because by default the dialogs are appended to the body (outside the form)
$('fieldset[data-transform=dialog]').each(function (index) {
$(this).dialog({
autoOpen: false,
appendTo:$(this).parent(),
modal: true,
height: 250,
width: 450,
buttons: {
Ok: function() {
$(this).dialog( 'close' );
}
},
});
});
});
$( '#bobContainer' ).trigger( 'loadContent');
}); //doc/ready
function add_category() {
var currentCount = $('#bob > fieldset').length;
var template = $('#bob > span').data('template');
template = template.replace(/__index__/g, currentCount);
template = template.replace(/__num__/g, (currentCount+1));
$('#bob').append(template).trigger( 'loadContent');
return false;
}
";
$this->headScript()
->appendFile("https://code.jquery.com/jquery-3.3.1.min.js",'text/javascript',array('integrity' => 'sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=','crossorigin'=>'anonymous'))
->appendFile("https://code.jquery.com/ui/1.12.1/jquery-ui.js")
->appendScript($script, $type = 'text/javascript', $attrs = array());
$this->headLink()
->appendStylesheet('https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css')
->appendStylesheet('https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css');
echo $this->headScript();
echo $this->headLink();
$title = 'Custom Collection Attributes';
$this->headTitle($title);
echo'<h1>'.$this->escapeHtml($title).'</h1>';
echo'<div class="container" id="bobContainer">';
$form->prepare();
echo $this->form()->openTag($form);
echo $this->formCollection($form);
$form->add([
'name' => 'submit',
'type' => 'submit',
'attributes' => [
'value' => 'Go',
'id' => 'submitbutton',
'class'=>'btn btn-success',
],
]);
$submit = $form->get('submit');
echo '<br>'.$this->formSubmit($submit);
echo $this->form()->closeTag();
echo'</div>';
ВЫХОД:
РЕДАКТИРОВАТЬ Я заметил небольшую проблему: когда элемент создается из шаблона, диалоги старых элементов становятся недоступными. Это связано с опцией диалога jquery modal: true
, Вероятно, есть исправление, но поскольку основной аргумент касается Zend... просто удалите эту опцию.
Я только что понял, что есть еще одно хорошее и несколько более гибкое решение: расширение элемента коллекции (почему я не подумал об этом раньше?).
Основным преимуществом этого подхода является то, что нет необходимости разделять имя элемента: "номер клона" ([0],[1],...
) напрямую доступен.
Особенности:
- Автонумерация меток и / или атрибутов
- Позволяет условное присвоение атрибутов / меток
- Позволяет взаимодействие между различными элементами (ограничено, см. Вопросы ниже)
- Работает с шаблонами (используя заполнители -> подробнее), и нет необходимости проверять, является ли индекс числом (это была проблема моего другого решения)
- Target_element может быть простым элементом, нет необходимости реализовывать
Zend/Form/Fieldset
Вопросы:
Установить идентификаторы может быть проблематично, так как (2)
из расширенного сценария нет способа получить доступ к имени конечного элемента (например:
fieldset[subfieldset][0][elementName]
), поскольку это будет иерархически построено позже.
Как это устроено:
1. Расширенная коллекция
//file: Application\src\Form\Element\ExtendedCollection.php
<?php
namespace Application\Form\Element;
use Zend\Form\Element\Collection;
class ExtendedCollection extends Collection
{
protected $autonumbering_callback = false;
protected $autonumbering_callback_options = [];
public function setOptions($options)
{
parent::setOptions($options);
if (isset($options['autonumbering_callback'])) {
$this->autonumbering_callback=(isset($options['autonumbering_callback'][0])) ? $options['autonumbering_callback'][0] : $options['autonumbering_callback'];
$this->autonumbering_callback_options=(isset($options['autonumbering_callback'][1])) ? $options['autonumbering_callback'][1] : [];
}
return $this;
}
protected function addNewTargetElementInstance($key)
{
//Original instructions
$this->shouldCreateChildrenOnPrepareElement = false;
$elementOrFieldset = $this->createNewTargetElementInstance();
$elementOrFieldset->setName($key);
$this->add($elementOrFieldset);
if (! $this->allowAdd && $this->count() > $this->count) {
throw new Exception\DomainException(sprintf(
'There are more elements than specified in the collection (%s). Either set the allow_add option ' .
'to true, or re-submit the form.',
get_class($this)
));
}
//Callback
if ($this->autonumbering_callback && method_exists(...$this->autonumbering_callback) && is_callable($this->autonumbering_callback)){
call_user_func_array($this->autonumbering_callback,[$elementOrFieldset,$key,$this->autonumbering_callback_options]);
}
return $elementOrFieldset;
}
}
2. Целевой элемент (здесь это набор полей, но может быть простым элементом)
//file: Application\src\Form\BobFieldset.php
<?php
namespace Application\Form;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
class BobFieldset extends Fieldset implements InputFilterProviderInterface {
private $inputFilter;
public function __construct($name = null) {
parent::__construct($name);
$this->setLabel('Answer __num__');
$this->setAttributes([
'title'=>'title no __num__',
'data-something'=>'custom attribute no __num__',
]);
$this->add(array(
'name' => 'text',
'type' => 'text',
'options' => array(
'label' => 'Text',
),
));
$this->add(array(
'name' => 'text2',
'type' => 'text',
'options' => array(
'label' => 'Text',
),
));
$this->add(array(
'name' => 'text3',
'type' => 'text',
'options' => array(
'label' => 'Text',
),
));
}
public function getInputFilterSpecification() {
return array(/*...*/);
}
}//class
3. Форма (с некоторым примером обратного вызова)
//file: Application\src\Form\BobForm.php
<?php
namespace Application\Form;
use Zend\Form\Form;
use Zend\Form\Fieldset; //needed for myCallback3
use Application\Form\Element\ExtendedCollection;
class BobForm extends Form
{
private $inputFilter;
public function __construct($name = null)
{
parent::__construct($name);
$this->add([
'name' => 'answer',
'type' => ExtendedCollection::class,
'options' => [
'count' =>3,
'should_create_template' => true,
'target_element' => new \Application\Form\BobFieldset2 ,
'autonumbering_callback'=>[
[$this,'myCallback'],
['attributes'=>['title','data-something'],'whateverYouWant'=>'something',]
],
],
]);
}
public function myCallback($elementOrFieldset, $key, $params){
foreach($params['attributes'] as $attr){
$autoNumAttr=str_replace('__num__',($key),$elementOrFieldset->getAttribute($attr));
$elementOrFieldset->setAttribute($attr,$autoNumAttr);
}//foreach
$label = str_replace('__num__',($key+1),$elementOrFieldset->getLabel());
$elementOrFieldset->setLabel($label);
}
public function myCallback2($elementOrFieldset, $key, $params){
$char='a';
foreach(range(1,$key) as $i) {
if($key>0){$char++;}
}
$elementOrFieldset->setLabel('Answer '.$char);
}
public function myCallback3($elementOrFieldset, $key, $params, $isChild=null){
if(!$isChild){$elementOrFieldset->setLabel('Answer '.($key+1));}
else{$elementOrFieldset->setLabel($key);}
//don't forget: use Zend\Form\Fieldset;
if($elementOrFieldset instanceof Fieldset && !$isChild){
$char='a';
foreach($elementOrFieldset as $item){
$this->myCallback3($item,($key+1 .$char++.') '),null,1);
}
}
}
}
ВЫХОД
target_element
должен ссылаться на набор полей. Это может быть либо новый экземпляр в форме, где находится коллекция, либо имя класса.
Например:
$fieldset = new Fieldset();
$fieldset->add([
'name' => 'some_field_name',
'type' => 'text',
]);
$this->add([
'name'=>'test',
'type' => Element\Collection::class,
'options' => [
'label' => 'MyCollection',
'count' => 6,
'should_create_template' => true,
'target_element' => $fieldset
],
]);
или же
$this->add([
'name'=>'test',
'type' => Element\Collection::class,
'options' => [
'label' => 'MyCollection',
'count' => 6,
'should_create_template' => true,
'target_element' => '\Namespace\Form\MyTextFieldset',
],
]);
Что касается настройки метки для каждого ввода, я еще не нашел способ сделать это.
Не слишком уверен во внутренней работе того, как работает коллекция, но я подозреваю, что она создает столько же новых экземпляров target_element
как требуется. С точки зрения простого добавления числа к метке (или произвольному атрибуту), вы можете создать класс fieldset со статическим свойством, которое начинается с 1
добавить его к метке и увеличить его значение?
Например:
namespace Module\Form;
use Zend\Form\Fieldset;
class MyFieldset extends Fieldset {
public static $instance_count = 1;
public function __construct() {
parent::__construct();
$this->add([
'name' => 'question',
'type' => 'text',
'attributes' => [
'alt' => 'input' . MyFieldset::$instance_count,
],
'options' => [
'label' => 'Text element No ' . MyFieldset::$instance_count,
],
]);
MyFieldset::$instance_count++;
}
}