Reputation: 1620
I am trying to create a form that contains nested collections. I don't know how to handle the JS part to display the children collection. Does anybody know how can I do that ?
Here is the code of my forms :
class ParentFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('case', 'choice', array(
'choices' => array(
'case1' => 'case1',
'case2' => 'case2',
'case3' => 'case3',
)))
->add ('subForm1', 'collection', array (
'type' => new Sub1FormType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'prototype' => true,
))
;
$builder->add('save',"submit") ;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
}
public function getName() {
return 'formtestparenttype';
}
}
class Sub1FormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('fieldSub1',"text" )
->add ('childForm1', 'collection', array (
'type' => new Sub2FormType,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'prototype' => true,
))
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
}
public function getName() {
return 'formtestsub1type';
}
}
class Sub2FormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('fieldSub2',"text" )
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver) {
}
public function getName() {
return 'formtesttype';
}
}
The controller :
$form = $this->createForm(new ParentFormType() ) ;
return $this->render('MyBundle:Test:test.html.twig', array(
'form' => $form->createView()
));
And Here; the twig + js part :
{% extends '::base.html.twig' %}
{% block content %}
{{ form_start(form) }}
<h3>Tags</h3>
<ul class="collectionHolder" data-prototype="{{ form_widget(form.subForm1.vars.prototype)|e }}">
{# iterate over each existing tag and render its only field: name #}
{% for subForm1 in form.subForm1 %}
<li>{{ form_row(subForm1) }} </li>
<ul class="collectionHolder" data-prototype="{{ form_widget(subForm2.vars.prototype)|e }}">
{%for subForm2 in subForm1.subForm2 %}
<li>{{ form_row(subForm2) }}</li>
{% endfor %}
{% endfor %}
</ul>
{{ form_end(form) }}
<script>
var $collectionHolder;
// setup an "add a tag" linkd
var $addTagLink = $('<a href="#" class="add_tag_link">Add</a>');
var $newLinkLi = $('<li></li>').append($addTagLink);
jQuery(document).ready(function() {
function addTagForm($collectionHolder, $newLinkLi)
{
// Get the data-prototype explained earlier
var prototype = $collectionHolder.data('prototype');
// get the new index
var index = $collectionHolder.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
$collectionHolder.data('index', index + 1);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
}
$collectionHolder = $('.collectionHolder');
$collectionHolder.append($newLinkLi);
// count the current form inputs we have (e.g. 2), use that as the new
// index when inserting a new item (e.g. 2)
$collectionHolder.data('index', $collectionHolder.find(':input').length);
$addTagLink.on('click', function(e) {
e.preventDefault();
addTagForm($collectionHolder, $newLinkLi);
});
});
</script>
{% endblock content %}
Upvotes: 8
Views: 3040
Reputation: 3
In case someone else comes across this issue. I experienced the same trouble and here what I think is the reason of this behaviour.
Please feel free to correct me, if my case isn't supposed to be the regular Symfony behaviour, and is due to an error from me.
Tanks to Rein Baarsma, I figured that my issue was within the prototype process.
Why symfony doesn't create 2nd, 3rd ... nth child in the collection, is because the prototype of the second level collection isn't "blank", like the first level one.
The first level collection "blank" prototype would look like this (for the input's part) :
someObject_collection___name___
,
where ___name___
is to be replaced by the collection index, hence the replace(/__name__/g, index)
in the documentation's JavaScript.
BUT for the second level collection, the prototype isn't "blank", but generated with the related element's index of the first level collection like : someObject_collection_1__otherCollection_1_
, instead of what I thought would be someObject_collection_1__otherCollection__name__
.
So when the replace
function is called for the second level collection, no match is found to replace ___name___
by the new child index.
The solution is to change the replace
call for the second level collection, to replace the element's index of the first level collection by the index of the current element of the second level collection.
Which would be something like : newForm.replace(/collection_\d/g, 'collection_' + index);
, for the label
tag's for
attribute, and the input
tag's id
attribute.
And something like : newForm.replace(/\[ligneCPackProds\]\[\d\]/g, '[collection][' + index + ']');
, for the input
tag's name
attribute.
By doing so, I was able to get all my children in my second level collection.
Upvotes: 0
Reputation: 1536
Your problem is that the example javascript was not written to handle multiple collections at once.
I've written a seperate javascript file which I always include when handling these form collections:
// js/form.collection.js
function FormCollection(div_id)
{
// keep reference to self in all child functions
var self=this;
self.construct = function () {
// set some shortcuts
self.div = $('#'+div_id);
self.div.data('index', self.div.find(':input').length);
// add delete link to existing children
self.div.children().each(function() {
self.addDeleteLink($(this));
});
// add click event to the Add new button
self.div.next().on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// add a new tag form (see next code block)
self.addNew();
});
};
/**
* onClick event handler -- adds a new input
*/
self.addNew = function () {
// Get the data-prototype explained earlier
var prototype = self.div.data('prototype');
// get the new index
var index = self.div.data('index');
// Replace '__name__' in the prototype's HTML to
// instead be a number based on how many items we have
var newForm = prototype.replace(/__name__/g, index);
// increase the index with one for the next item
self.div.data('index', index + 1);
// Display the form in the page in an li, before the "Add a tag" link li
self.div.append($(newForm));
// add a delete link to the new form
self.addDeleteLink( $(self.div.children(':last-child')[0]) );
// not a very nice intergration.. but when creating stuff that has help icons,
// the popovers will not automatically be instantiated
//initHelpPopovers();
return $(newForm);
};
/**
* add Delete icon after input
* @param Element row
*/
self.addDeleteLink = function (row) {
var $removeFormA = $('<a href="#" class="btn btn-danger" tabindex="-1"><i class="entypo-trash"></i></a>');
$(row).find('select').after($removeFormA);
row.append($removeFormA);
$removeFormA.on('click', function(e) {
// prevent the link from creating a "#" on the URL
e.preventDefault();
// remove the li for the tag form
row.remove();
});
};
self.construct();
}
Within the desired template I then simply target the collections by id and add instantiate the FormCollection by id, such as:
{% extends '::base.html.twig' %}
{% block content %}
{{ form_start(form) }}
<h3>Tags</h3>
<ul id="col-subform1" data-prototype="{{ form_widget(form.subForm1.vars.prototype)|e }}">
{# iterate over each existing tag and render its only field: name #}
{% for subForm1 in form.subForm1 %}
<li>{{ form_row(subForm1) }} </li>
<ul id="col-subform2" data-prototype="{{ form_widget(subForm2.vars.prototype)|e }}">
{%for subForm2 in subForm1.subForm2 %}
<li>{{ form_row(subForm2) }}</li>
{% endfor %}
{% endfor %}
</ul>
{{ form_end(form) }}
<script type="text/javascript" src="/js/form.collection.js"></script>
<script type="text/javascript">
new FormCollection('col-subform1');
new FormCollection('col-subform2');
</script>
Upvotes: 4