Search code examples
collectionstwigsymfony3

How to properly display multiple CollectionType fields using symfony3.x


Hello I'm trying to model an Entity that uses several different collections. I tried out the https://github.com/ninsuo/symfony-collection project and it offers a wide range of useful options. The closest example I've seen is the one with Collection of Collections where one Entity has several collections of the SAME child EntityType. I'm trying to achieve the same behaviour with several collections of DIFFERENT child EntityTypes.

The problem I face with my Entity is that when I put only one collection in it, the code works ok, but when I add a second collection of a different child Entity and send my form, my controller code ends up deleting the other collection's elements. I narrowed it down to the view, hence why I'm asking about that specific project.

I'm currently using Symfony 3.x, and have been able to follow the listed examples up to the point of working well with one collection only, I'm able to ADD, DELETE and UPDATE.

My controller code:

namespace SigavFileBundle\Form;

use Ivory\CKEditorBundle\Form\Type\CKEditorType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use SigavFileBundle\Entity\BookingAccommodation;
use SigavFileBundle\Entity\BookingIncludes;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\MoneyType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;

class BookingType extends AbstractType
{
    /**
    * {@inheritdoc}
    */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('flights', CollectionType::class, 
                array(
                    'entry_type' => FlightDataType::class,
                    'allow_add'    => true,
                    'allow_delete' => true,
                    'prototype'    => true,
                    'required'     => false,
                    'by_reference' => true,
                    'delete_empty' => true,
                    'prototype_name' => '__flights-collection__',
                    'attr'         => [
                        'class' => "flights-collection",
                    ],
                )
            )
            ->add('accommodations', CollectionType::class, 
                array(
                    'entry_type' => BookingAccommodationType::class,
                    'allow_add'    => true,
                    'allow_delete' => true,
                    'prototype'    => true,
                    'required'     => false,
                    'by_reference' => true,
                    'delete_empty' => true,
                    'prototype_name' => '__accomm-collection__',
                    'attr'         => [
                        'class' => "accomm-collection",
                    ],
                )
            )
            ->add('cars', CollectionType::class, 
                array(
                    'entry_type' => BookingCarType::class,
                    'allow_add'    => true,
                    'allow_delete' => true,
                    'prototype'    => true,
                    'required'     => false,
                    'by_reference' => true,
                    'delete_empty' => true,
                    'prototype_name' => '__cars-collection__',
                    'attr'         => [
                        'class' => "cars-collection",
                    ],
                )
            )
            ->add('transfers', CollectionType::class, 
                array(
                    'entry_type' => BookingTransferType::class,
                    'allow_add'    => true,
                    'allow_delete' => true,
                    'prototype'    => true,
                    'required'     => false,
                    'by_reference' => true,
                    'delete_empty' => true,
                    'prototype_name' => '__transfers-collection__',
                    'attr'         => [
                        'class' => "transfers-collection",
                    ],
                )
            )
            ->add('excursions', CollectionType::class, 
                array(
                    'entry_type' => BookingExcursionType::class,
                    'allow_add'    => true,
                    'allow_delete' => true,
                    'prototype'    => true,
                    'required'     => false,
                    'by_reference' => true,
                    'delete_empty' => true,
                    'prototype_name' => '__exc-collection__',
                    'attr'         => [
                        'class' => "exc-collection",
                    ],
                )
            )
            ->add('includes', CollectionType::class, 
                array(
                    'entry_type' => BookingIncludesType::class,
                    'allow_add'    => true,
                    'allow_delete' => true,
                    'prototype'    => true,
                    'required'     => false,
                    'by_reference' => true,
                    'delete_empty' => true,
                    'prototype_name' => '__inc-collection__',
                    'attr'         => [
                        'class' => "inc-collection",
                    ],
                )
            )
            ->add('customActivities', CollectionType::class, 
                array(
                    'entry_type' => BookingCustomActivityType::class,
                    'allow_add'    => true,
                    'allow_delete' => true,
                    'prototype'    => true,
                    'required'     => false,
                    'by_reference' => true,
                    'delete_empty' => true,
                    'prototype_name' => '__act-collection__',
                    'attr'         => [
                        'class' => "act-collection",
                    ],
                )
            )
            ->add('guides', CollectionType::class, 
                array(
                    'entry_type' => BookingGuideType::class,
                    'allow_add'    => true,
                    'allow_delete' => true,
                    'prototype'    => true,
                    'required'     => false,
                    'by_reference' => true,
                    'delete_empty' => true,
                    'prototype_name' => '__guides-collection__',
                    'attr'         => [
                        'class' => "guides-collection",
                    ],
                )
            )
            ->add('commentaries', CollectionType::class, 
                array(
                    'entry_type' => BookingCommentariesType::class,
                    'allow_add'    => true,
                    'allow_delete' => true,
                    'prototype'    => true,
                    'required'     => false,
                    'by_reference' => true,
                    'delete_empty' => true,
                    'prototype_name' => '__comm-collection__',
                    'attr'         => [
                        'class' => "comm-collection",
                    ],
                )
            )
        ;
    }

    /**
    * {@inheritdoc}
    */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'SigavFileBundle\Entity\Booking'
        ));
    }

    /**
    * {@inheritdoc}
    */
    public function getBlockPrefix()
    {
        return 'sigavfilebundle_booking';
    }
}

As you can see, multiple collections of different types. Up next, this is the code for two of those FormTypes only, BookingAccommodationType and BookingCarType:

BookingAccommodationType:

namespace SigavFileBundle\Form;

use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use SigavGeneralBundle\Controller\MealPlanController;
use SigavGeneralBundle\Entity\Hotel;
use SigavGeneralBundle\Entity\RoomType;
use SigavGeneralBundle\SigavGeneralBundle;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Doctrine\ORM\EntityRepository;

class BookingAccommodationType extends AbstractType
{
    /**
    * {@inheritdoc}
    */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        ...
        // Attributes go here
        ...
    }
    
    /**
    * {@inheritdoc}
    */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'SigavFileBundle\Entity\BookingAccommodation'
        ));
    }

    /**
    * {@inheritdoc}
    */
    public function getBlockPrefix()
    {
        return 'sigavfilebundle_bookingaccommodation';
    }
}

BookingCarType:

namespace SigavFileBundle\Form;

use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\TimeType;

class BookingCarType extends AbstractType
{
    /**
    * {@inheritdoc}
    */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        ...
        // Attributes go here
        ...
    }
    
    /**
    * {@inheritdoc}
    */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'SigavFileBundle\Entity\BookingCar'
        ));
    }

    /**
    * {@inheritdoc}
    */
    public function getBlockPrefix()
    {
        return 'sigavfilebundle_bookingcar';
    }
}

All view related code follows.

main_view.html.twig:

<!-- A lot of HTML code before //-->
{%
    form_theme form.accommodations
    'jquery.collection.html.twig'
    'booking/bookingAccommodations.html.twig'
%}

{{ form( form.accommodations) }}

<!-- A lot of HTML code between //-->

{%
form_theme form.cars
'jquery.collection.html.twig'
'booking/bookingCars.html.twig'
%}
{{ form( form.cars) }}

<!-- A lot of HTML code after //-->

<script>
    function initCollectionHolders( $id, $form_id )
    {
        $($id).collection({
            name_prefix:  $form_id,
            add_at_the_end: true,
            allow_add: 1,
            allow_duplicate: 1,
            allow_remove: 1,
            duplicate: '<a href="#"><span class="pe-7s-copy"></span></a>',
            add: '<a href="#"><span class="pe-7s-plus"></span></a>',
            remove: '<a href="#"><span class="pe-7s-close-circle"></span></a>'
        });
    }

    initCollectionHolders('.accomm-collection', '{{ form.accommodations.vars.full_name }}');
    initCollectionHolders('.cars-collection', '{{ form.cars.vars.full_name }}');
<script>

bookingCars.html.twig:

{% block sigavfilebundle_bookingcar_label %}{% endblock %}

{% block sigavfilebundle_bookingcar_widget %}

    {# HERE GOES THE ENTIRE FORM LAYOUT #} 

{% endblock %} 

bookingAccommodations.html.twig:

{% block sigavfilebundle_bookingaccommodation_label %}{% endblock %}

{% block sigavfilebundle_bookingaccommodation_widget %}

{# HERE GOES THE ENTIRE FORM LAYOUT #}

{% endblock %}

jquery.collection.html.twig:

{% block collection_widget %}
    {% spaceless %}
        {% if prototype is defined %}
            {% set attr = attr|merge({'data-prototype': form_row(prototype)}) %}
            {% set attr = attr|merge({'data-prototype-name': prototype.vars.name}) %}
        {% endif %}
        {% set attr = attr|merge({'data-allow-add': allow_add ? 1 : 0}) %}
        {% set attr = attr|merge({'data-allow-remove': allow_delete ? 1 : 0 }) %}
        {% set attr = attr|merge({'data-name-prefix': full_name}) %}
        {{ block('form_widget') }}
    {% endspaceless %}
{% endblock collection_widget %}

I wanted to know first if the symfony-collection library can be used in an environment with one Entity and multiple different child type collections

Thanks in advance ...


Solution

  • Following the discussion on GitHub,

    name_prefix option is only used on nested collections, it will help generating new entries using prototypes without conflicting between two or more collections.

    What you are looking for is the prefix option, used to prefix all selectors generated by symfony-collection plugin. In your case, prefix is the same for all collections, so a click on one or another button will trigger actions on the same collection.

    If you wish to create several collections on the same page, you'll need to change the collection prefix in order for the plugin to trigger the right actions for the right collection.

    For example:

     $('.collectionA').collection({
        'prefix': 'first-collection'
     });
    
     $('.collectionB').collection({
        'prefix': 'second-collection'
     });
    

    Then if you want to edit those collections form theme, you'll need to replace collection-add by first-collection-add on your add buttons for exmaple.

    <a href="#" class="first-collection-add btn btn-default">
        <span class="glyphicon glyphicon-plus-sign"></span>
    </a>
    

    See this demo for a running example.