Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Symfony2 : Radio buttons in a collection

In my application, I created a form using the collection field type :

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

With some JQuery, this code works correctly, but now I would like to select one of this dynamic tag to make it "the main tag".

In my Tag entity, I added a boolean attribute which define if the tag is the main or not :

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

But in my view, each row now contains a checkbox. So I can select more than one main tag. How to transform this checkbox in radio button please ?

like image 721
ncrocfer Avatar asked Jan 08 '14 16:01

ncrocfer


1 Answers

You're not tackling the problem from the right angle. If there should be a main tag, then this property should not be added in the Tag entity itself, but in the entity that contains it!

I'm speaking of the data_class entity related to the form having the tags attribute. This is the entity that should have a mainTag property.

If defined properly, this new mainTag attribute will not be a boolean, for it will contain a Tag instance, and thus will not be associated to a checkbox entry.

So, the way I see it, you should have a mainTag property containing your instance and a tags property that conatins all other tags.

The problem with that is that your collection field will no longer contain the main tag. You should thus also create a special getter getAllTags that will merge your main tag with all others, and change your collection definition to:

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

Now, how do we add the radio boxes, you may ask? For this, you will have to generate a new field:

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

These are the basics however, it only grows more complex from here. The real problem here is how your form is displayed. In a same field, you mix the usual display of a collection and the display of a choice field of the parent form of that collection. This will force you to use form theming.

To allow some room to reusability, you need to create a custom field. The associated 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
}

The form type:

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,
        ));
   }
}

Finally, in the form theme template:

{% 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 %}

Lastly, we need to include that in your parent entity, the one that originally contained tags:

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;
    }

    ...
}

And in your original form:

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

I recognize the solution I propose is verbose, however it seems to me to be the most efficient. What you are trying to do cannot be done easily in Symfony.

You can also note that there is an odd "prototype" option in the comment. I just wanted to underline a very useful property of "collection" in your case: the prototype option contains a blank item of your collection, with placeholders to replace. This allow to quickly add new items in a collection field using javascript, more info here.

like image 99
Zephyr Avatar answered Oct 06 '22 01:10

Zephyr