Home > Software design >  How can I create another, child-dependant select using Symfony Form Events?
How can I create another, child-dependant select using Symfony Form Events?

Time:09-08

I wanted to ask how I can achieve dynamic cascading children dependency using a structure similar to what is given in Symfony cookbook: dynamic_form_modification_using_form_events but with an additional field, which is being populated by data based on which option was chosen by the user. I managed to make it work with dependency similar to sports->position, but I can't manage to make it work with structure categories->categoryChildren->childCategoryChildren, where both categoryChildren and childCategoryChildren are dynamically created. When I try to make an event listener for categoryChildren, I'm getting an error:

The child with the name "categoryChildren" does not exist.

This is my form type:

/**
 * Class AddAdType
 * @package App\Ad\AdBundle\Form\Type
 */
class CreateAdType extends AbstractType
{
    /**
     * @param CategoryRepository $categoryRepository
     */
    public function __construct(
        CategoryRepository $categoryRepository,
    )
    {
        $this->categoryRepository = $categoryRepository;
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('description')
            ->add('price')
            ->add('category', EntityType::class, [
                'class' => Category::class,
                'choices' =>  $this->categoryRepository->findBy(['parent' => null]),
                'choice_label' => 'name',
                'required' => true,
                'placeholder' => 'Wybierz kategorie',
            ]);

        $this->categoryChildrenListener($builder);
        $this->childCategoryChildrenListener($builder);
    }

    /**
     * @param FormBuilderInterface $builder
     */
    public function categoryChildrenListener(FormBuilderInterface $builder)
    {
        $formModifier = function (FormInterface $form, Category $category = null) {
            $categoryChildren = null === $category ? [] : $this->categoryRepository->findBy(['parent' => $category]);

            $form->add('categoryChildren', EntityType::class, [
                'class' => Category::class,
                'choice_label' => 'name',
                'choices' => $categoryChildren,
                'mapped' => false,
                'attr' => [
                    'onChange' => "getNewVal(this);"
                ]
            ]);
        };

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formModifier) {
                $data = $event->getData();

                $formModifier($event->getForm(), $data->getCategory());
            }
        );

        $builder->get('category')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($formModifier) {
                $ad = $event->getForm()->getData();

                $formModifier($event->getForm()->getParent(), $ad);
            }
        );
    }

    /**
     * @param FormBuilderInterface $builder
     */
    public function childCategoryChildrenListener(FormBuilderInterface $builder)
    {
        $formChildCategoryModifier = function (FormInterface $form, Category $categoryChild = null) {
        $childCategoryChildren = null === $categoryChild ? [] : $this->categoryRepository->findBy(['parent' => $categoryChild]);

        $form->add('childCategoryChildren', EntityType::class, [
            'class' => Category::class,
            'choice_label' => 'name',
            'choices' => $childCategoryChildren,
            'mapped' => false
            ]);
        };

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formChildCategoryModifier) {
                $data = $event->getData();

                $formChildCategoryModifier($event->getForm(), $data->getCategory());
            }
        );

        $builder->get('categoryChildren')->addEventListener(
            FormEvents::POST_SUBMIT,
            function (FormEvent $event) use ($formChildCategoryModifier) {
                $ad = $event->getForm()->getData();

                $formChildCategoryModifier($event->getForm()->getParent(), $ad);
            }
        );
    }

    /**
     * @param OptionsResolver $resolver
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => Ad::class,
            'csrf_protection' => false
        ]);
    }
}

And this is my entity:

/**
 * Class Ad
 * @package App\Ad\AdBundle\Entity\Ad
 *
 * @ORM\Entity
 * @ORM\Table(name="ad")
 */
class Ad
{
    //[...]

    /**
     * @ORM\ManyToOne(targetEntity="App\Ad\CategoryBundle\Entity\Category")
     * @ORM\JoinColumn(name="category_id", nullable=false, referencedColumnName="id")
     */
    private $category;

    private $categoryChildren;

    private $childCategoryChildren;

    //[...]

    /**
     * @return Category|null
     */
    public function getCategory(): ?Category
    {
        return $this->category;
    }

    /**
     * @param Category|null $category
     * @return $this
     */
    public function setCategory(?Category $category): self
    {
        $this->category = $category;

        return $this;
    }

    /**
     * @param Category|null $categoryChildren
     */
    public function setCategoryChildren(?Category $categoryChildren): self
    {
        $this->categoryChildren = $categoryChildren;

        return $this;
    }

    /**
     * @return Category|null
     */
    public function getCategoryChildren(): ?Category
    {
        return $this->categoryChildren;
    }

    /**
     * @param Category|null $childCategoryChildren
     */
    public function setChildCategoryChildren(?Category $childCategoryChildren): self
    {
        $this->childCategoryChildren = $childCategoryChildren;

        return $this;
    }

    /**
     * @return Category|null
     */
    public function getChildCategoryChildren(): ?Category
    {
        return $this->childCategoryChildren;
    }
}

This is my ajax for category select:

let $category = $('#create_ad_category');
let $childCategory = $('#create_ad_categoryChildren');
checkEmptySelect();

if ( $category.change() ) {
    $category.change(function() {
        let $form = $(this).closest('form');
        let data = {};
        data[$category.attr('name')] = $category.val();
        $.ajax({
            url : $form.attr('action'),
            type: $form.attr('method'),
            data : data,
            complete: function(html) {
                $('#create_ad_categoryChildren').replaceWith(
                    $(html.responseText).find('#create_ad_categoryChildren')
                );
                checkEmptySelect()
                $('#create_ad_categoryChildren').prepend('<option value="" selected="selected"> '  
                    'Select a subcategory of '   $category.find("option:selected").text()   ' ...</option>');
            }
        });
    });
}

function checkEmptySelect() {
    if ( !$('#create_ad_categoryChildren').val()  ) {
        $('.subcategories').hide();
        // $('.childCategories').hide();
    } else {
        $('.subcategories').show();
    }
}

And this is my ajax for categoryChildren field:

function getNewVal(item) {
    let $form = $(this).closest('form');
    let data = {};
    data[$category.attr('name')] = $category.val();
    data[$childCategory.attr('name')] = item.value;

    $.ajax({
        url : $form.attr('action'),
        type: $form.attr('method'),
        data : data,
        complete: function(html) {

            $('#create_ad_childCategoryChildren').replaceWith(
                $(html.responseText).find('#create_ad_childCategoryChildren')
            );
            checkEmptySelect()
            $('#create_ad_childCategoryChildren').prepend('<option value="" selected="selected"> '  
                'Select a subcategory of '   $('#create_ad_categoryChildren').find("option:selected").text()   ' ...</option>');
        }
    });
}

I'm using Symfony 6.0. Any help will be appreciated, cheers.

CodePudding user response:

Solved; I changed an Event from POST_SUBMIT to PRE_SUBMIT in class CreateAdType, and changed logic in listeners. Also, I had to change an Ajax type in function getNewVal() from $form.attr('method') to 'POST', since form was trying to call 'GET' instead of 'POST'. Solution below:

CreateAdType.php:

//[...]
    /**
     * @param FormBuilderInterface $builder
     */
    public function categoryChildrenListener(FormBuilderInterface $builder)
    {
        $formModifier = function (FormInterface $form, Category $category = null) {
            $categoryChildren = null === $category ? [] : $this->categoryRepository->findBy(['parent' => $category]);

            $form->add('categoryChildren', EntityType::class, [
                'class' => Category::class,
                'label' => 'Podkategorie',
                'choice_label' => 'name',
                'choices' => $categoryChildren,
                'mapped' => false,
                'attr' => [
                    'onChange' => "getNewVal(this);"
                ]
            ]);
        };

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formModifier) {
                $data = $event->getData();

                $formModifier($event->getForm(), $data->getCategory());
            }
        );

        $builder->addEventListener(
            FormEvents::PRE_SUBMIT,
            function (FormEvent $event) use ($formModifier) {
                $ad = $event->getData();
                $categoryId = array_key_exists('category', $ad) ? $ad['category'] : null;
                $category = $this->categoryRepository->find($categoryId);

                $formModifier($event->getForm(), $category);
            }
        );
    }

    /**
     * @param FormBuilderInterface $builder
     */
    public function childCategoryChildrenListener(FormBuilderInterface $builder)
    {
        $formChildCategoryModifier = function (FormInterface $form, Category $categoryChild = null) {
        $childCategoryChildren = null === $categoryChild ? [] : $this->categoryRepository->findBy(['parent' => $categoryChild]);

        $form->add('childCategoryChildren', EntityType::class, [
            'class' => Category::class,
            'label' => 'Podkategorie podkategorii',
            'choice_label' => 'name',
            'choices' => $childCategoryChildren,
            'mapped' => false
            ]);
        };

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) use ($formChildCategoryModifier) {
                $data = $event->getData();

                $formChildCategoryModifier($event->getForm(), $data->getCategoryChildren());
            }
        );

        $builder->addEventListener(
            FormEvents::PRE_SUBMIT,
            function (FormEvent $event) use ($formChildCategoryModifier) {
                $ad = $event->getData();
                $childCategory = null;

                if ( array_key_exists('categoryChildren', $ad) ) {
                    if ( $ad['categoryChildren'] !== '' ) {
                        $childCategoryId = $ad['categoryChildren'];
                        $childCategory = $this->categoryRepository->find($childCategoryId);
                    }
                }
                $formChildCategoryModifier($event->getForm(), $childCategory);
            }
        );
    }
//[...]

Ajax call for categoryChildren field:

function getNewVal(item) {
    let $form = $(this).closest('form');
    let data = {};
    data[$category.attr('name')] = $category.val();
    data[$childCategory.attr('name')] = item.value;

    $.ajax({
        url : $form.attr('action'),
        type: "POST",
        data : data,
        complete: function(html) {
            $('#create_ad_childCategoryChildren').replaceWith(
                $(html.responseText).find('#create_ad_childCategoryChildren')
            );
            checkChildCategoryChildrenValue()
            $('#create_ad_childCategoryChildren').prepend('<option value="" selected="selected"> '  
                'Wybierz podkategorie kategorii '   $('#create_ad_categoryChildren').find("option:selected").text()   ' ...</option>');
        }
    });
}

function checkChildCategoryChildrenValue() {
    if ( !$('#create_ad_childCategoryChildren').val()  ) {
        $('.childCategories').hide();
    } else {
        $('.childCategories').show();
    }
}

I managed to implement some of the ideas from this post: How to add an Event Listener to a dynamically added field using Symfony Forms Hope it helps someone to save time and stress :)

  • Related