Abstract background with ethereal blue hues and floating geometric shapes, suggesting modernity and technological innovation, ideal for content related to Symfony web development.

Symfony: Custom Reusable FormType for Adding and Removing CollectionType Items

06/10/2022

This article describes how to write a reusable FormType, that allows you to add and remove rows in your CollectionType form fields. The instruction is based on Symfony 5 and PHP 7.4, using Symfony forms and Twig.

The final effect: use the custom FormType (named CustomCollectionType in this tutorial) so that prototype adding and removing is handled automatically wherever you use it.

Basic implementation

1. Create CustomCollectionType

In this step, you need to create your new FormType:

It is important that your CustomCollectionType returns the regular CollectionType as its parent, so it inherits the normal CollectionType behavior, options, and the way form data is processed.

As you want to manipulate the contents of the collection, you need to set some default_options in order not to provide them every time you use this CustomCollectionType in one of your forms.

You also need to assign a custom BlockPrefix to your CustomCollectionType - you can name it however you like, but you will need this exact BlockPrefix later on, so keep it in mind.

// src/Form/Type/CustomCollectionType.php

<?php

declare(strict_types=1);

namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\OptionsResolver\OptionsResolver;

class CustomCollectionType extends AbstractType
{
   public function getParent(): string
{
       return CollectionType::class;
   }
   public function configureOptions(OptionsResolver $resolver): void
   {
       $resolver->setDefaults([
           'allow_add' => true,
           'allow_delete' => true,
           'prototype' => true,
           'entry_options' => [
               'label' => false,
           ],
       ]);
   }
   public function getBlockPrefix(): string
   {
       return 'custom_collection';
   }
}

2. Create and configure a custom form layout

In this step, you need to tweak some Twig configuration, and create a new Twig file, in order to specify how this form type should be displayed and to create the universal JavaScript that will handle all your future CustomCollectionType forms.

First, let’s add a new block to your Twig file, and take a close look at the block name - it needs to start with your BlockPrefix from the previous step, and end with the “widget”.

// templates/form/customFormLayout.html.twig

{% block custom_collection_widget %}
{% endblock %}

Navigate to your Twig configuration, which normally is placed under config/packages/twig.yaml. It should contain the default_path to your templates and the default form_themes.

Modify your twig.yaml by adding your custom form layout, so it looks something like this - note, that the file path to your custom layout must match the newly created file:

// config/packages/twig.yaml

twig:
    default_path: '%kernel.project_dir%/templates'
    form_themes: ['bootstrap_4_layout.html.twig','form/customFormLayout.html.twig']

3. Fill your custom layout

Now let’s add some actual layout to render your custom collection type - the attributes such as “id” and “data-id” will be needed in the next step - to select and manipulate the elements using JavaScript.

Keep in mind that all the ids and data-ids need to be unique, in case you display more than one of your CustomCollectionType on one page. That is why you are going to use some of the form variables passed by Symfony - “form” and “id”. On a side note, you can get to any option passed to this form, using form.vars (for example form.vars.label).

Customize the view however you want - the most important parts are to set the id of the container div, list and set data-ids of the child form fields, proper data-action and data-target on the remove button, and proper id, data-prototype, and data-counter on the add button.

// templates/form/customFormLayout.html.twig

{% block custom_collection_widget %}
    <div id="{{ id }}">
        {% for childForm in form %}
            <div data-id="{{ childForm.vars.id }}" class="card p-4">
                <div class="row d-flex align-items-center">
                    <div class="col-10">
                        {{ form_widget(childForm) }}
                    </div>
                    <div class="col-2">
                        <div class="text-right">
                            <button 
                                type="button"
                                class="btn btn-danger mt-4"
                                data-action="{{ id }}remove" 
                                data-target="{{ childForm.vars.id }}"
                            >
                                Remove
                            </button>
                        </div>
                    </div>
                </div>
            </div>
        {% endfor %}
        <button
                type="button" 
                id="{{ id }}-add-prototype" 
                class="btn btn-info mb-4"
                data-prototype="{{form_widget(form.vars.prototype)|e('html_attr') }}"
                data-counter="{{ form|length }}"
        >
            Add
        </button>
    </div>
{% endblock %}

4. Make JavaScript do the work

Now let’s add some JavaScript, to be able to add new fields and remove the existing fields. Use the fact that you can inject the Twig variables as raw text in the file, for example, to create a unique JavaScript variable name for each form.

Note: This JavaScript creates div according to my layout of the form. Modify the created divs, buttons, their styles, and classes to suit your needs!

// templates/form/customFormLayout.html.twig

{% block custom_collection_widget %}
    <div id="{{ id }}" ...>
    <script>
        const {{id}}addListeners = (parent) => {
            const addButtons = parent.querySelectorAll('#{{ id }}-add-prototype');

            parent.querySelectorAll('[data-action="{{ id }}remove"]').forEach((element) => {
                element.addEventListener('click', () => {
                    document.querySelector(
                        '[data-id="'+ element.dataset['target'] + '"]'
                    ).remove();
                });
            });

            addButtons.forEach(function (addButton) {
                addButton.addEventListener('click', () => {
                    let counter = addButton.dataset['counter'];
                    let newRow = addButton.dataset['prototype'];

                    newRow = newRow.replace(/__name__/g, counter);

                    const div = document.createElement('div');
                    div.dataset.id = `{{ id }}_${counter}`;
                    div.classList.add('card');
                    div.classList.add('p-4');

                    const div2 = document.createElement('div');
                    div2.classList.add('col-10');
                    div2.dataset.class = '{{ form.vars.id }}';
                    div2.innerHTML = newRow;


                    const removeButtonDiv = document.createElement('div');
                    removeButtonDiv.innerHTML =
                            `<div class="text-right"><button type="button"` +
                            ` class="btn btn-danger mt-4" ` +
                            `data-action="{{ id }}remove" data-target="{{ id }}_${counter}">` +
                            `Add</button></div>`
                    ;
                    const div3 = document.createElement('div');
                    div3.classList.add('col-2');
                    div3.append(removeButtonDiv);

                    const div4 = document.createElement('div');
                    div4.classList.add('row');
                    div4.classList.add('d-flex');
                    div4.classList.add('align-items-center');

                    div4.append(div2);
                    div4.append(div3);
 
                   div.append(div4);

                    addButton.before(div);

                    {{id}}addListeners(div);

                    counter++;

                    addButton.dataset['counter'] = counter;
                })});
            };

        {{id}}addListeners(document);
    </script>
{% endblock %}

And voila! Now, all you need to do is to use your newly created CustomCollectionType in one of your forms, and it will automatically allow you to add and remove the child form fields:

// src/Form/Admin/Organization.php

<?php

declare(strict_types=1);

namespace App\Form\Admin\Organization;

use App\Document\Organization\Organization;
use App\Form\Type\CustomCollectionType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class OrganizationForm extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class)
            ->add('shops', CustomCollectionType::class, [
                'entry_type' => OrganizationShopForm::class,
            ])
            ->add('managers', CustomCollectionType::class, [
                'entry_type' => OrganizationManagerForm::class,
            ])
        ;
    }
}

Relevant sources:

Łu
Portret Łukasza Traczyka, back-end developera w Primotly, profesjonalisty o swobodnym i przystępnym usposobieniu, ubranego w jasnoniebieską koszulę.
Łukasz Traczyk
Back-end Developer

Najnowsze artykuły

Obraz koncepcyjny ukazujący kontrast między technologią AI a emisjami środowiskowymi, z symbolem mikroprocesora dla AI i chmurami symbolizującymi emisję CO2 nad budynkami przemysłowymi.

Innovations | 22/11/2024

Jak sztuczna inteligencja pomaga firmom śledzić emisje CO2?

Bernhard Huber

Rozliczanie emisji dwutlenku węgla, praktyka śledzenia, pomiaru i raportowania emisji gazów cieplarnianych (GHG) to tematy, które coraz częściej pojawiają się nie tylko w kontekście dbania o środowisko, ale też odpowiedzialności biznesu. Ponieważ 90% firm z listy Fortune 500 zobowiązało się do realizacji celów zrównoważonego rozwoju, zapotrzebowanie na skuteczne rozwiązania w zakresie rozliczania emisji dwutlenku węgla rośnie. Jednak firmy często zmagają się ze złożonymi łańcuchami dostaw i brakiem danych w czasie rzeczywistym, co sprawia, że dokładne rozliczanie emisji dwutlenku węgla jest trudnym zadaniem. Wygodne rozwiązanie podsuwa sztuczna inteligencja. Oferuje ona potężny zestaw narzędzi do automatyzacji i optymalizacji śledzenia emisji oraz identyfikowania nieefektywności.

Szczegóły
Grafika symbolizująca ład korporacyjny w kontekście ESG

Business | 15/11/2024

Czym jest ład korporacyjny w kontekście ESG?

Łukasz Kopaczewski

Podczas gdy wszystkie trzy filary - środowiskowy, społeczny i ład korporacyjny - są niezbędne, ład korporacyjny często odgrywa najbardziej fundamentalną rolę. Zarządzanie, które obejmuje etyczne przywództwo, przejrzystość i odpowiedzialność, zapewnia, że wysiłki ESG nie są tylko deklaracjami na papierze, ale są zintegrowane z codziennymi decyzjami firmy. Warto zauważyć, że niedawne badanie wykazało, że 39% firm uważa, że osiąga odpowiednie wyniki w zakresie zarządzania, co wskazuje na znaczne możliwości poprawy.

Szczegóły
Ilustracja do artykułu o wykorzystaniu sztucznej inteligencji (AI) w biznesowych procesach decyzyjnych w przedsiębiorstwie

Business | 08/11/2024

Wykorzystanie sztucznej inteligencji (AI) w biznesowych procesach decyzyjnych w przedsiębiorstwie

Agata Pater

Poleganie wyłącznie na intuicji w biznesie może często oznaczać utratę dużych możliwości. Najlepszym przykładem jest Netflix. Analizując ponad 30 milionów dziennych „odtworzeń” oraz niezliczone oceny i wyszukiwania subskrybentów, Netflix dokładnie określił, czego chcą widzowie, co doprowadziło do stworzenia hitowych seriali, takich jak House of Cards. To podejście nie tylko zwiększyło zaangażowanie widzów; zrewolucjonizowało podejście branży rozrywkowej do tworzenia treści.

Szczegóły