Stuck when trying to render a polycollection
WishCow opened this issue · 10 comments
I'm following the docs from here:
Instead of using an inbuilt collection, I'm trying to use the polycollection, provided by this bundle. I have everything configured, but the form doesn't seem to render the necessary tags/attributes that is needed for the javascript helper. I have the following form setup:
$builder->add('questions', 'infinite_form_polycollection', [
'types' => [ 'choice_question', 'open_question' ],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
]);
And in my template:
{% block _tp_assignment_questions_entry_row %}
<div class="item input-append">
{{ form_widget(form) }}
<a class="btn btn-danger remove_item"><i class="icon-minus"></i></a>
</div>
{% endblock %}
My form is named _tp_assignment. The markup that is in the _tp_assignment_questions_entry_row, is not present in the generated HTML code anywhere. AFAIK, there is no such fragment as "entry_row" in Symfony, so I have tried all kinds of combinations, such as collection_row, collection_widget, _tp_assignment_collection_row, etc. but none of these seem to generate the correct markup that the javascript helper expects.
Can you help out a bit please?
WishCow,
I'm having similar struggles to you also...
I'm also trying to use the JS collection helper in combination with the polycollection.
I'll post what I have in my Twig template so far in case it helps you at all:
{% extends 'MNMPolyCollectionTrialBundle::layout.html.twig' %}
{% form_theme form.items _self %}
{% block body %}
<h1>Poly Collection Trial</h1>
{{ form_start(form) }}
{{ form_row(form.items) }}
{{ form_end(form) }}
{% endblock %}
{% block _item_list_type_items_entry_row %}
<div class="item input-append">
{{ form_widget(form) }}
<a class="btn btn-danger remove_item">Del</a>
</div>
{% endblock _item_list_type_items_entry_row %}
{% block infinite_form_polycollection_row %}
{% set line = attribute(form.vars.prototypes, 'mnm_poly_collection_item_book_form') %}
<a class="add_item btn btn-primary" data-prototype="{{ form_row(line) | escape }}" href="#" style="margin-bottom: 0;"><i class="icon-plus icon-white"></i> Add Book</a>
{{ block('lines_footer') }}
<div class="items">
{{ form_widget(form) }}
</div>
{% endblock infinite_form_polycollection_row %}
So far with this I can see that the prototype is getting through into the HTML, but it doesn't seem to be responding to any clicks to add a new row.
I had all this working fine with the collection type, just not with polycollection.
If you're able to get any further with it I would be very interested to see a full working example...
@WishCow Did you set {% form_theme form.questions _self %}? The entry_row should work although if your rows are very different then it may be more useful to define open_question_row and choice_question_row instead.
@marshmn It's also necessary to create a new window.infinite.Collection object in your Javascript.
I threw together a quick working example based on WishCow's code and put the important files in a gist: https://gist.github.com/jmclean/8013037 The twig code is at the bottom.
Merk and I have been considering writing a demo app to clarify usage for this reason. :)
Thank you very much for your example. Saving seems to work now, but when I try to load an existing Assignment, that has Questions attached to it, I get an exception, related to data_class:
The form's view data is expected to be an instance of class ME\TeachersPortalBundle\Entity\ChoiceQuestion, but is an instance of class ME\TeachersPortalBundle\Entity\OpenQuestion. You can avoid this error by setting the "data_class" option to null or by adding a view transformer that transforms an instance of class ME\TeachersPortalBundle\Entity\OpenQuestion to an instance of ME\TeachersPortalBundle\Entity\ChoiceQuestion.
The data_class for my OpenQuestionType is 'ME\TeachersPortalBundle\Entity\OpenQuestion', and for the ChoiceQuestionType it is 'ME\TeachersPortalBundle\Entity\ChoiceQuestion'. It seems it's trying to assign an entity to the wrong subform?
In addition to the data_class option you need to set a model_class option on OpenQuestionType and ChoiceQuestionType. The model_class can be the same as the data_class in nearly all cases, being different only in the case of some data transformers.
Oh right, it's in the example too, sorry. I'm going to try this tomorrow.
Am I right in suspecting that this model_class is something specific to the polycollection, and not to symfony's form component?
Yes. Originally the polycollection used the data_class but this causes a problem in some cases. data_class is a Symfony form option that is used to verify the view data, amongst other things. However, polycollection chooses a form type based on the model data.
I ran into a case where I needed a data transformer, and the model data and view data were entirely different classes. If I set data_class to the model class, then polycollection would choose the correct form type but then Symfony would complain that the view data was wrong. If I set data_class to the view class, then polycollection wouldn't be able to find the correct type. So the only solution was to introduce a new option.
Okay, great, I've managed to proceed a bit further. New rows are being added to the form, but my ChoiceQuestion form has a "collection" field, that gets an incorrect prototype, which makes all the validations fail.
// ChoiceQuestionType
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('question')
->add('choices', 'collection', array(
'type' => 'text',
'allow_add' => true,
'allow_delete' => true,
'attr' => [ 'class' => 'choices'],
'options' => [
'label' => 'choice',
'required' => true
]
))
->add('correctChoice', 'hidden')
->add('_type', 'hidden', [
'data' => $this->getName(),
'mapped' => false
])
->add('submit', 'submit');
}
The form works perfectly fine if I use it a standalone form, with the help of the infinite javascript helper. However, I also want to use it as a subform of an Assignment:
// AssignmentType
$builder->add('name', 'text', [
'label' => 'Article name',
'max_length' => 500,
'attr' => [ 'class' => 'asg-elem input-xxlarge' ]
])
->add('url', 'text', [
'label' => 'Article link',
'attr' => [
'class' => 'asg-elem input-xxlarge',
'placeholder' => 'e.g., http://www.nytimes.com/'
]
])
->add('questions', 'infinite_form_polycollection', [
'types' => [ 'choice_question', 'open_question' ],
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false
]);
When using the standalone version, this is the prototype that gets generated on the "add" button (for adding choices to a choice_question):
<div>
<label for="choice_question_choices___name__" class="required">choice</label>
<input type="text" id="choice_question_choices___name__" name="choice_question[choices][__name__]" required="required" />
</div>
This is correct, and working as intended, but if I create an AssignmentType form, and add a ChoiceQuestionType into it dynamically, this is generated prototype:
<div>
<label for="tp_assignment_questions_0_choices_0" class="required">choice</label><input type="text" id="tp_assignment_questions_0_choices_0" name="tp_assignment[questions][0][choices][0]" required="required" />
</div>
The second one is missing the name markers.
TLDR;
I have an Assignment form that has multiple OpenQuestions, and ChoiceQuestions. A ChoiceQuestion can have multiple choices (where choice is a simple textbox, with no entity attached). When click on the add choice question button, to dynamically add a new ChoiceQuestion to the form, the "add" button (that I would use to add new choices to it) for the newly inserted ChoiceQuestion contains an incorrect prototype.
You'll have to use a different prototype_name when you have nested collections:
// ChoiceQuestionType::buildForm
->add('choices', 'collection', array(
'type' => 'text',
'allow_add' => true,
'allow_delete' => true,
'prototype_name' => '__choice_name__',
'attr' => ['class' => 'choices', 'data-prototype-name' => '__choice_name__'],
'options' => [
'label' => 'choice',
'required' => true
]
))
Then in your Javascript, pass the new prototype name to the the Collection constructor. The PHP code above also adds a data-prototype-name attribute that you can use in your initialisation code.
new window.infinite.Collection(collection, prototypes, {
'prototypeName': (collection.attr('data-prototype-name') || '__name__')
})
Great, it seems to work now, thank you very much.
Have you thought about proposing this bundle to the core Form component? Because I would love to see it integrated. I ran into bschussek in a random blog post, where he advised opening a feature request about this, which I did, and mentioned your bundle.
We'll consider that, or at least fix up our documentation. :)
For now, I think we can call this issue closed.