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(); |
|
}) |