Geolim4
Geolim4

Reputation: 454

Symfony 3 form collection with sub-collection

I have a form with a field named "planes".

This field is can be multiple (collection) with the following sub-fields: - speed - weight - color - engines

The subfield "engines" is also a collection of field: - rpm - fuel_type - weight

I'm wondering what would the Symfony form builder would looks like since the user should have the possibility to add as much planes as he want. The same apply for engines.

Upvotes: 1

Views: 1433

Answers (1)

Andrew Vakhniuk
Andrew Vakhniuk

Reputation: 594

It will look like that:

builder in class which has planes collection:

public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('planes', CollectionType::class, array(
            'entry_type' => PlaneType::class,
            'allow_add'    => true,
            'allow_delete' => true, 
            'by_reference' => false,// this will call get/set of your entity
            'prototype' => true, // is needed coz there will be 2 prototypes 
            'prototype_name'=> '__planes__'//1-st prototype name
            'attr' => [
                'class' => 'embeddedCollection'
            ]

        ));
    }

builder in PlaneType class:

public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('speed');
        $builder->add('weight');
        $builder->add('color');
        $builder->add('engines', CollectionType::class, array(
            'entry_type' => EngineType::class,
            'allow_add'    => true,
            'allow_delete' => true, 
            'by_reference' => false,// this will call get/set of your entity
            'prototype' => true, // is needed coz there will be 2 prototypes 
            'prototype_name'=> '__engines__',//2 -nd prototype
            'attr' => [
                'class' => 'subEmbeddedCollection'
            ]
        ));
    }

builder in EngineType class:

public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('rpm');
        $builder->add('fuel_type');
        $builder->add('weight');
    }

Also you should add some javascript for dynamic adding/removing fields, during adding you should exchange prototype names with the id of number of the fields.

EDIT: Some js code with JQuery to make it work on front-end:

function initCollection(collectionHolder, addItemButton, deleteItemButton, prototypePattern) {
    let $addItemButton = $(addItemButton);
    // Get the ul that holds the collection of tags
    let $collectionHolder = $(collectionHolder);
    // add a delete link to all of the existing tag form li elements
    $collectionHolder.children('.form-group').each(function () {
        addFormDeleteLink($(this), deleteItemButton);
    });
    // add the "add a tag" anchor and li to the tags ul
    $collectionHolder.append($addItemButton);
    // 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);
    //when new element is added then trigger event on collection
    let addNewElementEvent = jQuery.Event ('element-added');
    let addNewChildCollection = jQuery.Event ('new-child-collection');
    $addItemButton.on('click', function (e)  {
        // add a new tag form (see next code block)
        lastAddedElement = addForm($collectionHolder, $addItemButton, deleteItemButton, prototypePattern);
        if($collectionHolder.hasClass('embeddedCollection')) {
            $collectionHolder.trigger(addNewElementEvent);
        }else{
            $collectionHolder.trigger(addNewChildCollection);
        }
    });
}

function addForm($collectionHolder, $addItemButton, deleteItemButton, prototypePattern) {
    // Get the data-prototype explained earlier
    let prototype = $collectionHolder.data('prototype');
    // get the new index
    let index = $collectionHolder.data('index');
    let newForm = prototype;
    newForm = newForm.replace(prototypePattern, 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
    let $newFormLi = $(newForm);
    $addItemButton.before($newFormLi);
    addFormDeleteLink($newFormLi, deleteItemButton);
    return $newFormLi;
}

function addFormDeleteLink($tagFormLi, deleteItemButton) {
    let $removeFormButton = $(deleteItemButton);
    $tagFormLi.append($removeFormButton);

    $removeFormButton.on('click', function (e) {
        // remove the li for the tag form
        let $parent = $tagFormLi.parent();
        $tagFormLi.remove();

        if($removeFormButton.hasClass('labels-rewrite'))
        {
            $parent.trigger(jQuery.Event('labels-rewrite'));
        }

    });
}

The parent collection should have class embeddedCollection, then the event 'element-added' will be trigerred if a new child was added. In case new plane with the child collection of engines is added you need to call initCollection method on that added engines collection.

So first you need to init existing collections, example:

//init all sub collections of engines of the planes 
$('.subEmbeddedCollection').each(function () {
    initCollection($(this),
    '<button type="button">Add engine</button>',
    '<button type="button" >Delete engine</button>',
        /__engines__/g
    );
});
//init main collection of planes
let $collectionHolder = $('.embeddedCollection');
initCollection($collectionHolder,
    '<button type="button">Add plane</button>',
    '<button type="button" >Delete plane</button>',
    /__planes__/g
); 

And then you should init all dynamically added sub collecitons of engines when the event element-added is triggered in the main collection of planes:

//init collection in added plane
$collectionHolder.on('element-added', function () {

    let $lastChildCollection = $(this).find('.engines').last();
    initCollection($lastChildCollection,
    '<button type="button">Add engine</button>',
    '<button type="button" >Delete engine</button>',
        /__engines__/g
    );
});

Upvotes: 4

Related Questions