php – How to change options of select dynamically?

I’m trying to create an application with Symfony, which aims to allow a user to manage his budget, by creating, removing and editing transactions. I have created my project, and also my entities with Doctrine, everything is well for now, the project works perfectly with Crud and database.

But, I have a problem, as you can see on the following picture, a new transaction is created with a form, with the following inputs:

a namean amounta type and a category. A type is either a debit or a credit, and the input category represents the usage of the transaction (salary, bills, shopping, etc.)

My problem is that I would like to adapt the options of the Category select dynamically, depending on the value of the Type select (for example, if credit is chosen, it shows salary, and if it’s debit, then the options will be bills and shopping).

I know that the best way to proceed is to use AJAX, but I have some problems implementing it. Indeed, I already developed the adaptation of the Category options depeding on the value setted for the Type select (it works well, as I wish), but only on load of the webpage.

Now, I would like to trigger this same event on change with AJAX, and this is where I struggle… I tried some codes, but every time, there is no change that is happening, even if console.log shows me that the code doesn’t encounter any issue. Here is my code:

templatestransactionnew.html.twig

{% extends 'base.html.twig' %}

{% block title %}New Transaction{% endblock %}

{% block body %}
    <h1>Create new Transaction</h1>

    {{ form(form)}}

    <button type="submit" class="btn" formonvalidate>Valider</button>

    <a href="{{ path('app_transaction_index') }}">back to list</a>
{% endblock %}

srcRepositoryCategoryRepository.php

<?php

namespace AppRepository;

use AppEntityCategory;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrinePersistenceManagerRegistry;
use AppEntityType;

/**
 * @extends ServiceEntityRepository<Category>
 *
 * @method Category|null find($id, $lockMode = null, $lockVersion = null)
 * @method Category|null findOneBy(array $criteria, array $orderBy = null)
 * @method Category[]    findAll()
 * @method Category[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class CategoryRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Category::class);
    }

    public function add(Category $entity, bool $flush = false): void
    {
        $this->getEntityManager()->persist($entity);

        if ($flush) {
            $this->getEntityManager()->flush();
        }
    }

    public function remove(Category $entity, bool $flush = false): void
    {
        $this->getEntityManager()->remove($entity);

        if ($flush) {
            $this->getEntityManager()->flush();
        }
    }

    public function findByTypeOrderedByAscName(Type $type): array
    {
        return $this->createQueryBuilder('c')
            ->andWhere('c.type = :type')
            ->setParameter('type', $type)
            ->orderBy('c.title', 'ASC')
            ->getQuery()
            ->getResult()
        ;
    }
}

srcFormTransactionType.php

<?php

namespace AppForm;

use AppEntityTransaction;
use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilderInterface;
use SymfonyComponentOptionsResolverOptionsResolver;
use SymfonyComponentFormFormEvents;
use SymfonyComponentFormFormEvent;
use SymfonyBridgeDoctrineFormTypeEntityType;
use AppRepositoryTypeRepository;
use AppRepositoryCategoryRepository;

class TransactionType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name')
            ->add('montant')
            ->add('type')
            ->add('category')
        ;
    }

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

srcControllerTransactionController.php

<?php

namespace AppController;

use AppEntityTransaction;
use AppFormTransactionType;
use AppRepositoryTransactionRepository;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;
use SymfonyComponentFormFormEvents;
use SymfonyComponentFormFormEvent;
use SymfonyBridgeDoctrineFormTypeEntityType;
use AppRepositoryTypeRepository;
use AppRepositoryCategoryRepository;
use SymfonyComponentValidatorConstraintsNotBlank;

#[Route('/transaction')]
class TransactionController extends AbstractController
{
    #[Route("https://stackoverflow.com/", name: 'app_transaction_index', methods: ['GET'])]
    public function index(TransactionRepository $transactionRepository): Response
    {
        return $this->render('transaction/index.html.twig', [
            'transactions' => $transactionRepository->findAll(),
        ]);
    }

    #[Route('/new', name: 'app_transaction_new', methods: ['GET', 'POST'])]
    public function new(Request $request, TypeRepository $typeRepository, CategoryRepository $categoryRepository): Response
    {
        $form = $this->createFormBuilder(['type' => $typeRepository->find(0)])
            ->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($categoryRepository) {
                $type = $event->getData()['type'] ?? null;

                $categories = $type === null ? [] : $categoryRepository->findByTypeOrderedByAscName($type);

                $event->getForm()->add('category', EntityType::class, [
                    'class' => 'AppEntityCategory',
                    'choice_label' => 'title',
                    'choices' => $categories,
                    'disabled' => $type === null,
                    'placeholder' => "Sélectionnez une catégorie",
                    'constraints' => new NotBlank(['message' => 'Sélectionnez une catégorie'])
                ]);
            })
            ->add('name')
            ->add('montant')
            ->add('type', EntityType::class, [
                'class' => 'AppEntityType',
                'choice_label' => 'title',
                'placeholder' => "Sélectionnez un type",
                'constraints' => new NotBlank(['message' => 'Sélectionnez un type'])
            ])
            ->getForm();

        return $this->renderForm('transaction/new.html.twig', compact('form'));
    }

    #[Route('/{id}', name: 'app_transaction_show', methods: ['GET'])]
    public function show(Transaction $transaction): Response
    {
        return $this->render('transaction/show.html.twig', [
            'transaction' => $transaction,
        ]);
    }

    #[Route('/{id}/edit', name: 'app_transaction_edit', methods: ['GET', 'POST'])]
    public function edit(Request $request, Transaction $transaction, TransactionRepository $transactionRepository): Response
    {
        $form = $this->createForm(TransactionType::class, $transaction);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $transactionRepository->add($transaction, true);

            return $this->redirectToRoute('app_transaction_index', [], Response::HTTP_SEE_OTHER);
        }

        return $this->renderForm('transaction/edit.html.twig', [
            'transaction' => $transaction,
            'form' => $form,
        ]);
    }

    #[Route('/{id}', name: 'app_transaction_delete', methods: ['POST'])]
    public function delete(Request $request, Transaction $transaction, TransactionRepository $transactionRepository): Response
    {
        if ($this->isCsrfTokenValid('delete'.$transaction->getId(), $request->request->get('_token'))) {
            $transactionRepository->remove($transaction, true);
        }

        return $this->redirectToRoute('app_transaction_index', [], Response::HTTP_SEE_OTHER);
    }
}

Attempt for AJAX

$(document).on('change', '#form_type', function() {
    const $type = $('#form_type');
    const $form = $(this).closest('form');
    $.ajax({
        url: $form.attr('action'),
        type: $form.attr('method'),
        data: $form.serializeArray(),
        success: function (html) {
            $('#form_category').replaceWith($(html).find('#form_category'));
        }
    });
});

PS: I would also like the Category select to be disabled when the Type select’s placeholder is selected, and the Category select to be enabled when a value is selected with the Type select.

The main code is in the public function new() of TransactionController.php.

Leave a Comment