Symfony2: радио кнопки в коллекции

В моем приложении я создал форму с использованием типа поля collection:

$builder->add('tags', 'collection', array(
   'type' => new TagType(),
   'label' => false,
   'allow_add' => true,
   'allow_delete' => true,
   'by_reference' => false
));

С некоторыми JQuery этот код работает правильно, но теперь я хотел бы выбрать один из этих динамических тегов, чтобы сделать его "основным тегом".

В моей сущности Tag я добавил логический атрибут, который определяет, является ли тег основным или нет:

/**
 * @ORM\Column(name="main", type="boolean")
 */
private $main;

Но, на мой взгляд, каждая строка теперь содержит флажок. Поэтому я могу выбрать более одного основного тега. Как преобразовать этот флажок в переключатель, пожалуйста?

Ответ 1

Вы не решаете проблему под прямым углом. Если должен быть основной тег, это свойство не должно добавляться в тег, а в сущности, которая его содержит!

Я говорю об объекте data_class, связанном с формой, имеющей атрибут теги. Это объект, который должен иметь свойство mainTag.

Если он определен правильно, этот новый атрибут mainTag не будет логическим, поскольку он будет содержать экземпляр Тег и, следовательно, не будет связан с записью флажка.

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

Проблема заключается в том, что поле вашей коллекции больше не будет содержать основной тег. Таким образом, вы также должны создать специальный getter getAllTags, который объединит ваш основной тег со всеми остальными и изменит определение вашей коллекции на:

$builder->add('allTags', 'collection', array(
    'type' => new TagType(),
    'label' => false,
    'allow_add' => true,
    'allow_delete' => true,
    'by_reference' => false
));

Теперь, как мы можем добавить радио-боксы, вы можете спросить? Для этого вам нужно создать новое поле:

$builder->add('mainTag', 'radio', array(
    'type' => 'choice',
    'multiple' => false,
    'expanded' => true,
    'property_path' => 'mainTag.id', // Necessary, for 'choice' does not support data_classes
));

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

Чтобы позволить некоторую возможность повторного использования, вам нужно создать настраиваемое поле. Связанный data_class:

class TagSelection
{
    private mainTag;

    private $tags;

    public function getAllTags()
    {
        return array_merge(array($this->getMainTag()), $this->getTags());
    }

    public function setAllTags($tags)
    {
        // If the main tag is not null, search and remove it before calling setTags($tags)
    }

    // Getters, setters
}

Тип формы:

class TagSelectionType extends AbstractType
{
    protected buildForm( ... )
    {
        $builder->add('allTags', 'collection', array(
            'type' => new TagType(),
            'label' => false,
            'allow_add' => true,
            'allow_delete' => true,
            'by_reference' => false
        ));

        // Since we cannot know which tags are available before binding or setting data, a listener must be used
        $formFactory = $builder->getFormFactory();
        $listener = function(FormEvent $event) use ($formFactory) {

            $data = $event->getForm()->getData();

            // Get all tags id currently in the data
            $choices = ...;
            // Careful, in PRE_BIND this is an array of scalars while in PRE_SET_DATA it is an array of Tag instances

            $field = $this->factory->createNamed('mainTag', 'radio', null, array(
                'type' => 'choice',
                'multiple' => false,
                'expanded' => true,
                'choices' => $choices,
                'property_path' => 'mainTag.id',
            ));
            $event->getForm()->add($field);
        }

        $builder->addEventListener(FormEvent::PRE_SET_DATA, $listener);
        $builder->addEventListener(FormEvent::PRE_BIND, $listener);
    }

    public function getName()
    {
        return 'tag_selection';
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'TagSelection', // Adapt depending on class name
            // 'prototype' => true,
        ));
   }
}

Наконец, в шаблоне темы формы:

{% block tag_selection_widget %}
    {% spaceless %}
    {# {% set attr = attr|default({})|merge({'data-prototype': form_widget(prototype)}) %} #}
    <ul {{ block('widget_attributes') }}>
        {% for child in form.allTags %}
        <li>{{ form_widget(form.mainTag[child.name]) }} {{ form_widget(child) }}</li>
        {% endfor %}
    </ul>
    {% endspaceless %}
{% endblock tag_selection_widget %}

Наконец, мы должны включить, что в родительском объекте, который первоначально содержал теги:

class entity
{
    // Doctrine definition and whatnot
    private $tags;

    // Doctrine definition and whatnot
    private $mainTag;

    ...
    public setAllTags($tagSelection)
    {
        $this->setMainTag($tagSelection->getMainTag());
        $this->setTags($tagSelection->getTags());
    }

    public getAllTags()
    {
        $ret = new TagSelection();
        $ret->setMainTag($this->getMainTag());
        $ret->setTags($this->getTags());

        return $ret;
    }

    ...
}

И в оригинальной форме:

$builder->add('allTags', new TagSelection(), array(
    'label' => false,
));

Я понимаю, что предлагаемое мной решение является подробным, однако, как мне кажется, оно является наиболее эффективным. То, что вы пытаетесь сделать, не может быть легко сделано в Symfony.

Вы также можете заметить, что в комментарии есть нечетная опция "prototype". Я просто хотел подчеркнуть очень полезное свойство "коллекции" в вашем случае: опция прототипа содержит пустой элемент вашей коллекции с заменяющими заполнителями. Это позволяет быстро добавлять новые элементы в поле коллекции с помощью javascript, больше информации здесь.

Ответ 2

Это неправильное решение, но поскольку вы используете jQuery для добавления/удаления...

TagType

->add('main', 'radio', [
    'attr' => [
        'class' => 'toggle'
        ],
     'required' => false
    ])

JQuery

div.on('change', 'input.toggle', function() {

    div
    .find('input.toggle')
    .not(this)
    .removeAttr('checked');
});

http://jsfiddle.net/coma/CnvMk/

И используйте ограничение обратного вызова, чтобы убедиться, что существует только один основной тег.

Ответ 3

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

Итак, самым простым решением здесь является создание нового свойства main_tag рядом с тегами в вашей сущности, создание скрытого поля main_tag (с идентификатором для тега Преобразователь данных) в вашей форме и заполнить и изменить это поле с помощью jQuery (например, установить его на тег, щелкнуть или очистить удаление основного тега)

Ответ 4

Возможно, что-то связано с опцией multiple form, но может потребоваться небольшая настройка вашей формы коллекции и объекта тега.