Home > OS >  How to change options of select dynamically?
How to change options of select dynamically?

Time:07-09

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 perfectly works 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 name, an amount, a type and a category. A type is either a debit or a credit, and the category input 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 chosed, 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:

templates\transaction\new.html.twig

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

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

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

    {{ form(form)}}

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

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

src\Repository\CategoryRepository.php

<?php

namespace App\Repository;

use App\Entity\Category;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use App\Entity\Type;

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

src\Form\TransactionType.php

<?php

namespace App\Form;

use App\Entity\Transaction;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use App\Repository\TypeRepository;
use App\Repository\CategoryRepository;

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,
        ]);
    }
}

src\Controller\TransactionController.php

<?php

namespace App\Controller;

use App\Entity\Transaction;
use App\Form\TransactionType;
use App\Repository\TransactionRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use App\Repository\TypeRepository;
use App\Repository\CategoryRepository;
use Symfony\Component\Validator\Constraints\NotBlank;

#[Route('/transaction')]
class TransactionController extends AbstractController
{
    #[Route('/', 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' => 'App\Entity\Category',
                    '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' => 'App\Entity\Type',
                '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.

CodePudding user response:

You can do this by creating a separate controller action with a different route, which returns the desired options. Your JavaScript would then use the returned data to repopulate the options in the form.

Note:

NOT TESTED, and needs more work! (may contain syntax errors, reference incorrect property names etc.) Adapt this sample flow to fit your needs.

Add the following to src\Controller\TransactionController.php

use Symfony\Component\HttpFoundation\JsonResponse;
#[Route('/type/options', name: 'app_transaction_type_options', methods: ['POST'])]
public function new(Request $request, TypeRepository $typeRepository, CategoryRepository $categoryRepository): Response
{
  // TODO: get the Type entity from post data
  // $type = $typeRepository->findOneBy...(
  return new JsonResponse($categoryRepository->findByTypeOrderedByAscName($type));
}

Add an input to templates\transaction\new.html.twig to get the path from.

    <input type="hidden" id="type_options_path" value="{{ path('app_transaction_type_options') }}">

Then your JavaScript would look something like this:

$(document).on('change', '#form_type', function() {
    const $type = $('#form_type');
    $.ajax({
        url: $('#type_options_path').val(),
        type: 'POST',
        data: {type: $type.val()},
        success: function (data) {
            const $category = $('#form_category');
            $category.find('option').detach();
            const opts = JSON.parse(data)
            for(const opt of opts){
              $(`<option value="${opt.id}">${opt.name}</option>`).appendTo($category)
            }
        }
    });
});

CodePudding user response:

Good day.

Dynamic Generation for Submitted Forms

Another case that can appear is that you want to customize the form specific to the data that was submitted by the user. For example, imagine you have a registration form for sports gatherings. Some events will allow you to specify your preferred position on the field. This would be a choice field for example. However the possible choices will depend on each sport. Football will have attack, defense, goalkeeper etc... Baseball will have a pitcher but will not have a goalkeeper. You will need the correct options in order for validation to pass.

The meetup is passed as an entity field to the form. So we can access each sport like this:

// src/AppBundle/Form/Type/SportMeetupType.php
namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
// ...

class SportMeetupType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('sport', 'entity', array(
                'class'       => 'AppBundle:Sport',
                'empty_value' => '',
            ))
        ;

        $builder->addEventListener(
            FormEvents::PRE_SET_DATA,
            function (FormEvent $event) {
                $form = $event->getForm();

                // this would be your entity, i.e. SportMeetup
                $data = $event->getData();

                $sport = $data->getSport();
                $positions = null === $sport ? array() : $sport->getAvailablePositions();

                $form->add('position', 'entity', array(
                    'class'       => 'AppBundle:Position',
                    'empty_value' => '',
                    'choices'     => $positions,
                ));
            }
        );
    }

    // ...
}

When you’re building this form to display to the user for the first time, then this example works perfectly.

Details are on - https://symfony-docs-zh-cn.readthedocs.io/cookbook/form/dynamic_form_modification.html#cookbook-form-events-submitted-data

  • Related