infinite-networks/InfiniteFormBundle

Stuck when trying to render a polycollection

WishCow opened this issue · 10 comments

I'm following the docs from here:

https://github.com/infinite-networks/InfiniteFormBundle/blob/master/Resources/doc/collection-helper.md

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.