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) %}
{% endmacro %} {{ form_start(form) }}New Sentence __name_plus_1__
{% for variation in form.sentence_variations %} {{ _self.sentence_variation_prototype(variation) }} {% endfor %}{{ form_end(form,{'render_rest': false}) }}{{ form_label(form.name) }}{{ form_widget(form.name) }}
Sentences
{# SHOW ALREADY SAVED ROWS #} {% for sentence in form.sentences %} {% set number = loop.index %}{% endfor %}Sentence {{ number }}
{% for sentence_variation in sentence.sentence_variations %} {% set variation_number = loop.index %} {{ _self.sentence_variation_prototype(sentence_variation) }} {% endfor %}
{{ form_widget(form.submit) }}
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(); })