In my application, I created a form using the collection
field type :
$builder->add(\'tags\', \'collection\', array(
\'type\' => new TagTyp
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.
Maybe there is something to do with the multiple form option, but it might require a little tweaking on your collection form and tag entity.
This is not the right solution, but since you are using jQuery to add/remove...
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/
And use a callback constraint to ensure that there is only one main tag.
First thing you should wary about - its that in your scheme if tag become main for one entity it will be main for all entities because the tag store attribute and few entities can be tagged with one tag.
So simplest decision here is to create new property main_tag
near tags in your entity, create hidden field main_tag
(with id to Tag Data transformer) in your form and populate and change this field with jQuery(for example set it on tag click or clear on main tag delete)