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) %}
    <div>{{ form_widget(form.text, { attr: { class: "form-control-sm mt-2" }}) }}</div>
{% endmacro %}

{% macro sentence_prototype(form) %}
<hr>
<div class="sentence_variation__name__ collection"
     data-index="{{ form.sentence_variations|length > 0 ? form.sentence_variations|last.vars.name + 1 : 0 }}"
     data-prototype="{{ _self.sentence_variation_prototype(form.sentence_variations.vars.prototype)|e('html_attr') }}"
>
<h5>New Sentence __name_plus_1__</h5>
<button type="button" class="add_item_link" data-collection-holder-class="sentence_variation__name__">Add a variation</button>
    {% for variation in form.sentence_variations %}
        {{ _self.sentence_variation_prototype(variation) }}
    {% endfor %}
</div>
{% endmacro %}

{{ form_start(form) }}
<div class="row">
    <div class="col-md-12 ">
        <div class="form-group">
            {{ form_label(form.name) }}
        </div>
        <div class="form-group">
            {{ form_widget(form.name) }}
        </div>
        
        <hr>
        <h3>Sentences</h3>
        <button type="button" class="add_item_link" data-collection-holder-class="sentences">Add a sentence</button>
        <div class="sentences collections" id="col-subform1"
             data-index="{{ form.sentences|length > 0 ? form.sentences|last.vars.name + 1 : 0 }}"
             data-prototype="{{ _self.sentence_prototype(form.sentences.vars.prototype)|e('html_attr') }}"
        >
{#    SHOW ALREADY SAVED ROWS        #}
            {% for sentence in form.sentences %}
                {% set number = loop.index %}
                <div class="sentence_variation_existing{{ number }} collection" id="col-subform2"
                     data-index="{{ sentence.sentence_variations|length > 0 ? sentence.sentence_variations|last.vars.name + 1 : 0 }}"
                     data-prototype="{{ _self.sentence_variation_prototype(sentence.sentence_variations.vars.prototype)|e('html_attr') }}"
                >
                <h4>Sentence {{ number }}</h4>
                <button type="button" class="add_item_link" data-collection-holder-class="sentence_variation_existing{{ number }}">Add a variation</button>

                {% for sentence_variation in sentence.sentence_variations %}
                    {% set variation_number = loop.index %}
                    {{ _self.sentence_variation_prototype(sentence_variation) }}
                    <button type="button" class="delete-button" data-collection-holder-class="template_sentences_{{ number-1 }}_sentence_variations_{{ variation_number-1 }}_text">Delete Variant</button>
                {% endfor %}
                </div>

            {% endfor %}

        </div>

        <hr>
    </div>


    <div class="col-md-6 form-group">
        {{ form_widget(form.submit) }}
    </div>
</div>
{{ 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.