Nested CollectionType Form

Example of nested collection forms
TemplateType.php

class TemplateType extends AbstractType
{


    public function buildForm(FormBuilderInterface $builder, array $options): void
    {

        $builder
            ->add('name', TextType::class, [
                'label' => "Template Name",
                'required' => true,
                'attr' => [
                    'class' => 'form-control',
                ],
            ])
            ->add('sentences', CollectionType::class, [
                'entry_type' => SentenceType::class,
                'entry_options' => ['label' => false],
                'label' => false,
                'allow_add' => true,
                'allow_delete' => true,
                'delete_empty' => true,
                'by_reference' => false,
                'prototype' => true,
                'prototype_name' => '__sentence__',
            ])
            ->add('submit', SubmitType::class, [
                'label' => 'Update',
                'attr' => [
                    'class' => "btn btn-sm btn-primary"
                ],
            ],
            )
        ;

    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Template::class,
            'csrf_protection' => false,
        ]);
    }

SentenceType.php

class SentenceType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {

        $builder
            ->add('sentence_variations', CollectionType::class, [
                'entry_type' => SentenceVariationType::class,
                'entry_options' => ['label' => "Sentence Variant"],
                'label' => false,
                'allow_add' => true,
                'allow_delete' => true,
                'by_reference' => false,
                'prototype' => true,
                'prototype_name' => '__variation__',
            ])
        ;

    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => Sentence::class,
            'csrf_protection' => false,
        ]);
    }
}

SentenceVariantType.php

class SentenceVariationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {

        $builder
            ->add('text', TextType::class, [
                'label' => false,
                'required' => true,
            ])
        ;

    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'data_class' => SentenceVariation::class,
            'csrf_protection' => false,
        ]);
    }
}

__form.html.twig

{% macro sentence_variation_prototype(form) %}
    
{{ form_widget(form.text, { attr: { class: "form-control-sm mt-2" }}) }}
{% endmacro %} {% macro sentence_prototype(form) %}
New Sentence __name_plus_1__
{% for variation in form.sentence_variations %} {{ _self.sentence_variation_prototype(variation) }} {% endfor %}
{% endmacro %} {{ form_start(form) }}
{{ form_label(form.name) }}
{{ form_widget(form.name) }}

Sentences

{# SHOW ALREADY SAVED ROWS #} {% for sentence in form.sentences %} {% set number = loop.index %}

Sentence {{ number }}

{% for sentence_variation in sentence.sentence_variations %} {% set variation_number = loop.index %} {{ _self.sentence_variation_prototype(sentence_variation) }} {% endfor %}
{% endfor %}

{{ form_widget(form.submit) }}
{{ form_end(form,{'render_rest': false}) }}

template_form.js where the magic happens

const addFormDeleteLink = (item) => {
    const removeFormButton = document.createElement('button');
    removeFormButton.className = 'delete-button';
    removeFormButton.innerText = 'Delete';

    item.append(removeFormButton);

    removeFormButton.addEventListener('click', (e) => {
        e.preventDefault();
        item.remove();
    });
}

const addFormToCollection = (e) => {
    const collectionHolder = document.querySelector('.' + e.currentTarget.dataset.collectionHolderClass);
    //*** - Added for nested functionality
    indexPlus1 = parseInt(collectionHolder.dataset.index) + 1;

    const item = document.createElement('div');

    item.innerHTML = collectionHolder
        .dataset
        .prototype
        .replace(/__name__/g, collectionHolder.dataset.index);

    //*** - Added for nested functionality
    item.innerHTML = item.innerHTML
        .replace(/__name_plus_1__/g, indexPlus1);
    //*** - Added for nested functionality
    if (item.innerHTML.includes("__sentence__")) {
        item.innerHTML = item.innerHTML
            .replace(/__sentence__/g, collectionHolder.dataset.index);
    } else {
        item.innerHTML = item.innerHTML
            .replace(/__variation__/g, collectionHolder.dataset.index);
    }

    collectionHolder.appendChild(item);

    collectionHolder.dataset.index++;

    // add a delete link to the new form
    addFormDeleteLink(item);
}

document
    .querySelectorAll('div.collections>div')
    .forEach((tag) => {
        addFormDeleteLink(tag)
    })

$(document).on('click', '.add_item_link', function (e) {
    addFormToCollection(e);
});

//*** - Added for nested functionality
$(document).on('click', '.delete-button', function (e) {
    $("#" + e.currentTarget.dataset.collectionHolderClass).parent().remove();
    e.currentTarget.remove();
})

Leave a Reply

Your email address will not be published. Required fields are marked *