Populate dependent select box using Symfony & Ajax

I was not able to find any solution matching my case scenario, so I decided to ask here:
Basically what I need to achieve is to render a form with several select boxes namely the Company, ProductsCategory, and Products. So depending on which category the user chooses, I want to filter and show only the products of that chosen category. I tried to follow the Symfony documentation as mentioned here , but I cannot get it to work. the front-end products select box remains blank even after a category has been set and also the ajax return with status 500 with error:

Return value of AppEntityProductsCategory::getProducts() must be an instance of AppEntityProducts or null, instance of DoctrineORMPersistentCollection returned

here are the codes:
My Exportables entity

namespace AppEntity;

use DoctrineORMMapping as ORM;

/**
 * @ORMEntity(repositoryClass="AppRepositoryExportablesRepository")
 */

class Exportables
{
/**
 * @ORMId()
 * @ORMGeneratedValue()
 * @ORMColumn(type="integer")
 */
private $id;

/**
 * @ORMManyToOne(targetEntity="AppEntityExportCompany", inversedBy="exportables")
 * @ORMJoinColumn(nullable=false)
 */
private $company;

/**
 * @ORMManyToOne(targetEntity="AppEntityProducts", inversedBy="exportables")
 * @ORMJoinColumn(nullable=false)
 */
private $product;

/**
 * @ORMColumn(type="boolean")
 */
private $isActive;

/**
 * @ORMManyToOne(targetEntity="AppEntityProductsCategory")
 * @ORMJoinColumn(nullable=false)
 */
private $category;

public function getId(): ?int
{
    return $this->id;
}

public function getCompany(): ?ExportCompany
{
    return $this->company;
}

public function setCompany(?ExportCompany $company): self
{
    $this->company = $company;

    return $this;
}

public function getProduct(): ?Products
{
    return $this->product;
}

public function setProduct(?Products $product): self
{
    $this->product = $product;

    return $this;
}

public function getIsActive(): ?bool
{
    return $this->isActive;
}

public function setIsActive(bool $isActive): self
{
    $this->isActive = $isActive;

    return $this;
}

public function getCategory(): ?ProductsCategory
{
    return $this->category;
}

public function setCategory(?ProductsCategory $category): self
{
    $this->category = $category;

    return $this;
}
}

ProductsCategory entity:

namespace AppEntity;

use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
use DoctrineORMMapping as ORM;

/**
 * @ORMEntity(repositoryClass="AppRepositoryProductsCategoryRepository")
 */
class ProductsCategory
{
/**
 * @ORMId()
 * @ORMGeneratedValue()
 * @ORMColumn(type="integer")
 */
private $id;

/**
 * @ORMColumn(type="string", length=50)
 */
private $categoryTitle;

/**
 * @ORMColumn(type="text", nullable=true)
 */
private $categoryDescription;

/**
 * @ORMColumn(type="boolean")
 */
private $isActive;

/**
 * @ORMOneToMany(targetEntity="AppEntityProducts", mappedBy="category", cascade={"persist", "remove"})
 */
private $products;


public function __construct()
{
    $this->product = new ArrayCollection();
}

public function getId(): ?int
{
    return $this->id;
}

public function getCategoryTitle(): ?string
{
    return $this->categoryTitle;
}

public function setCategoryTitle(string $categoryTitle): self
{
    $this->categoryTitle = $categoryTitle;

    return $this;
}

public function getCategoryDescription(): ?string
{
    return $this->categoryDescription;
}

public function setCategoryDescription(?string $categoryDescription): self
{
    $this->categoryDescription = $categoryDescription;

    return $this;
}

public function getIsActive(): ?bool
{
    return $this->isActive;
}

public function setIsActive(bool $isActive): self
{
    $this->isActive = $isActive;

    return $this;
}

public function getProducts(): ?Products
{
    return $this->products;
}

public function setProducts(Products $products): self
{
    $this->products = $products;

    // set the owning side of the relation if necessary
    if ($products->getCategory() !== $this) {
        $products->setCategory($this);
    }

    return $this;
}

public function __toString()
{
    return $this->categoryTitle;
}
}

Products entity:

namespace AppEntity;

use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
use DoctrineORMMapping as ORM;

/**
 * @ORMEntity(repositoryClass="AppRepositoryProductsRepository")
 */

class Products
{

/**
 * @ORMId()
 * @ORMGeneratedValue()
 * @ORMColumn(type="integer")
 */
private $id;

/**
 * @ORMColumn(type="string", length=50)
 */
private $productTitle;

/**
 * @ORMColumn(type="text")
 */
private $productDescription;

/**
 * @ORMColumn(type="boolean")
 */
private $isActive;

/**
 * @ORMManyToOne(targetEntity="AppEntityProductsCategory", inversedBy="products", cascade={"persist", "remove"})
 * @ORMJoinColumn(nullable=false)
 */
private $category;

/**
 * @ORMOneToMany(targetEntity="AppEntityExportables", mappedBy="product")
 */
private $exportables;

public function __construct()
{
    $this->exportables = new ArrayCollection();
}


public function getId(): ?int
{
    return $this->id;
}

public function getProductTitle(): ?string
{
    return $this->productTitle;
}

public function setProductTitle(string $productTitle): self
{
    $this->productTitle = $productTitle;

    return $this;
}

public function getProductDescription(): ?string
{
    return $this->productDescription;
}

public function setProductDescription(string $productDescription): self
{
    $this->productDescription = $productDescription;

    return $this;
}

public function getIsActive(): ?bool
{
    return $this->isActive;
}

public function setIsActive(bool $isActive): self
{
    $this->isActive = $isActive;

    return $this;
}

public function getCategory(): ?ProductsCategory
{
    return $this->category;
}

public function setCategory(ProductsCategory $category): self
{
    $this->category = $category;

    return $this;
}

/**
 * @return Collection|Exportables[]
 */
public function getExportables(): Collection
{
    return $this->exportables;
}

public function addExportable(Exportables $exportable): self
{
    if (!$this->exportables->contains($exportable)) {
        $this->exportables[] = $exportable;
        $exportable->setProduct($this);
    }

    return $this;
}

public function removeExportable(Exportables $exportable): self
{
    if ($this->exportables->contains($exportable)) {
        $this->exportables->removeElement($exportable);
        // set the owning side to null (unless already changed)
        if ($exportable->getProduct() === $this) {
            $exportable->setProduct(null);
        }
    }

    return $this;
}

public function __toString(){
    return $this->productTitle;
}
}

Exportables Type:

namespace AppForm;

use AppEntityProducts;
use AppEntityExportables;
use AppEntityProductsCategory;
use SymfonyComponentFormFormEvent;
use SymfonyComponentFormFormEvents;
use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormInterface;
use SymfonyComponentFormFormBuilderInterface;
use SymfonyBridgeDoctrineFormTypeEntityType;
use SymfonyComponentOptionsResolverOptionsResolver;
use SymfonyComponentFormExtensionCoreTypeChoiceType;

class ExportablesType extends AbstractType

{

public function buildForm(FormBuilderInterface $builder, array $options)
{

    $builder
        ->add('company')
        ->add('category', EntityType::class, array(
            'class' => ProductsCategory::class,
            'placeholder' => 'Select a Category...',
        ))
        ->add('isActive')
    ;

    $formModifier = function (FormInterface $form, ProductsCategory $cat = null) {
        $products = null === $cat ? [] : $cat->getProducts();
        dump($products);

        $form->add('product', EntityType::class, [
            'class' => 'AppEntityProducts',
            'placeholder' => '',
            'choices' => $products,
        ]);
    };

    $builder->addEventListener(
        FormEvents::PRE_SET_DATA,
        function (FormEvent $event) use ($formModifier) {
            // this would be your entity, i.e. SportMeetup
            $data = $event->getData();

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

    $builder->get('category')->addEventListener(
        FormEvents::POST_SUBMIT,
        function (FormEvent $event) use ($formModifier) {
            // It's important here to fetch $event->getForm()->getData(), as
            // $event->getData() will get you the client data (that is, the ID)
            $cat = $event->getForm()->getData();

            // since we've added the listener to the child, we'll have to pass on
            // the parent to the callback functions!
            $formModifier($event->getForm()->getParent(), $cat);
        }
    );

}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'data_class' => Exportables::class,
    ]);
}
}

ExportablesController:

namespace AppController;

use AppEntityExportables;
use AppFormExportablesType;
use DoctrineORMEntityManagerInterface;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentRoutingAnnotationRoute;
use SymfonyBundleFrameworkBundleControllerAbstractController;

class ExportablesController extends AbstractController
{

/**
 * @Route("/admin-panel/exportables/new", name="exportables_create")
 * @Route("/admin-panel/exportables/{id}/edit", name="exportables_edit")
 */
public function exportables_create_and_edit(Request $request, EntityManagerInterface $em, Exportables $exportables = null)
{
    if(!$exportables){
        $exportables = new Exportables();
    }
    $form = $this->createForm(ExportablesType::class, $exportables);
    $form->handleRequest($request);

    if($form->isSubmitted() && $form->isValid()){
        $em->persist($exportables);
        $em->flush();
    }
    return $this->render('/admin-panel/exportables_create.html.twig', [
        'exForm' => $form->createView(),
        'editMode' => $exportables->getId() !== null
    ]);
}   
}

Finally, the twig file rendering the form:

{% extends '/admin-panel/base-admin.html.twig' %}
{%  block body %}
{{ form_start(exForm) }}
<div class="form-group">
    {{ form_row(exForm.company, {'attr':{'class':"form-control"}}) }}
</div>
   <div class="form-group">
       {{ form_row(exForm.category, {'attr':{'class':"form-control"}}) }}
   </div>
{% if exForm.product is defined %}
   <div class="form-group">
       {{ form_row(exForm.product, {'label': "Product..",  'attr':{'class':"form-control"}}) }}
   </div>
{% endif %}

<div class="form-group">
    <div class="custom-control custom-checkbox">
        {{ form_widget(exForm.isActive, {'attr': {'class': "custom-control-input", 'checked': "checked"}}) }}
        <label class="custom-control-label" for="exportables_isActive">Visible on the website?</label>
    </div>
</div>
<button type="submit" class="btn btn-success">Create</button>

 {{ form_end(exForm) }}
{% endblock %}

{% block javascripts %}
<script>
    {# //for some reasons this line doesnt work  $(document).ready(function() { #}
    jQuery(document).ready(function($){
        var $cat = $('#exportables_category');
        // When cat gets selected ...
        $cat.change(function() {
            // ... retrieve the corresponding form.
            var $form = $(this).closest('form');
            // Simulate form data, but only include the selected cat value.
            var data = {};
            data[$cat.attr('name')] = $cat.val();
        console.log("cat val " + $cat.val());
        //console.log($form.attr('method'));
        const url = "{{ path('exportables_create')|escape('js') }}";
        //console.log(data);
        //console.log(url);
        //why undefined?? console.log($form.attr('action'));
            // Submit data via AJAX to the form's action path.
            $.ajax({
                //url : $form.attr('action'),
                url : url,
                type: $form.attr('method'),
                data : data,
                success: function(html) {
                // Replace current position field ...
                $('#exportables_product').replaceWith(
                    // ... with the returned one from the AJAX response.
                    //$(html).find('#exportables_product')
                    array(1,2,3)
                );
                // Position field now displays the appropriate positions.
                }
            });
        });
    });
    </script>
{% endblock %}

Any help is greatly appreciated.

Source: Symfony Questions

Was this helpful?

0 / 1

Leave a Reply 0

Your email address will not be published. Required fields are marked *