phpeventssymfony4craueformflow

How to use Symfony4 FormEvent onChange field with craue/CraueFormFlowBundle stepper to update or validate form?


  1. Context :

I'm using craue/CraueFormFlowBundle bundle in a Symfony4 application to generate stepper from basic SYmfony4 form and it work fine until I try to introduce form Event to modify my form depending of form changes :

How to Dynamically Modify Forms Using Form Events

  1. Code :

Controller :

**
 * @Route("/user/add", name="add_user")
 * @param Request $request
 */
public function add(Request $request){

    $employe = new Employe();
    $flow = $this->addUserFlow;
    $flow->bind($employe);

    // form of the current step
    $form = $flow->createForm();
    if ($flow->isValid($form)) {
        $flow->saveCurrentStepData($form);

        if ($flow->nextStep()) {
            // form for the next step
            $form = $flow->createForm();
        } else {

            //Persist user
            $this->manager->persist($employe);
            $this->manager->flush();

            //Reset flow to create new user
            $flow->reset();

            //Redirect to new user detail page
            return $this->redirect($this->generateUrl('user_edit', array('id' => $employe->getId())));
        }
    }

    return $this->render('app/users/users/add.html.twig', [
        'form' => $form->createView(),
        'flow' => $flow,
        'employe' => $employe
    ]);
}

AddUserFlow.php

class AddUserFlow extends FormFlow
{
    protected $allowDynamicStepNavigation = true;

    protected function loadStepsConfig()
    {
        return [
            [
                'label' => 'Informations générales',
                'form_type' => UserGeneralType::class,
            ],
            [
                'label' => 'Contact',
                'form_type' => EmployeContactType::class,
            ],
            [
                'label' => 'Contrat',
                'form_type' => EmployeContractType::class,
            ],
            [
                'label' => "Accès a l'application",
                'form_type' => EmployeAccessType::class,
            ]
        ];
    }

}

Here is my step 3 form which contains the FormEvent :

/**
 * Class ContractType
 * @package App\Form\User\Stepper
 */
class ContractType extends AbstractType
{

    /**
     * @var EntityManagerInterface
     */
    private $manager;

    /**
     * ContractType constructor.
     */
    public function __construct(EntityManagerInterface  $manager)
    {
        $this->manager = $manager;
    }

    /**
     * @param FormBuilderInterface $builder
     * @param array $options
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('type', EntityType::class, [
                "class" => TypeContract::class,
                'label'     => 'Type de contrat',
                "required" => true
            ])
            ->add('startDate', DateType::class, [
                'label'     => 'Date de début',
                'widget'    => 'single_text',
                "required"  => false,
            ])
            ->add('endDate', DateType::class, [
                'label'     => 'Date de fin',
                'widget'    => 'single_text',
                "required"  => false,
            ])
            ->add('isInsertion', CheckboxType::class, [
                'label'     => 'Insertion professionnelle',
                "required"  => false,
            ])
            ->add('hourlyRate', MoneyType::class, [
                'label'     => 'Taux horaire',
                "required"  => false,
            ])
        ;

        $formContractTypeModifier = function (FormInterface $form, $contractType = null) {
            if($contractType != null && !$contractType instanceof TypeContract){
                $contractType = $this->manager->getRepository(TypeContract::class)->find($contractType);
            }

            if($contractType != null && $contractType instanceof TypeContract && $contractType->getSlug() === ContractTypeEnum::INTERIM){
                $form->add('company', EntityType::class, [
                    "class" => Company::class,
                    "label"     => "Entreprise",
                    "required"  => true,
                ]);
            }
        };

        // Events listener to modify the form regarding PRE_SET_DATA datas
        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($formContractTypeModifier){
            /** @var array $data */
            $data = $event->getData();
            /** TypeContract $contractType */
            $contractType = isset($data['type']) ? $data['type'] : null;
            //Add elements to form
            $formContractTypeModifier($event->getForm(), $contractType);
        });

        // Events listener to modify the form regarding PRE_SUBMIT
        $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($formContractTypeModifier){
            /** @var array $data */
            $data = $event->getData();
            /** TypeContract $contractType */
            $contractType = isset($data['type']) ? $data['type'] : null;
            //Add elements to form
            $formContractTypeModifier($event->getForm(), $contractType);
        });

    }

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

Here is my add_user.html.twig view :

{% if flow.getCurrentStepNumber() == 3 %}
    {% include 'app/users/users/stepper/_step_3.html.twig' %}
{% else %}
    {{ form_errors(form) }}
    {{ form_rest(form) }}
{% endif %}

The step_3.html.twig view is like this :

{{ form_row(form.contracts.type) }}

{% if form.contracts.company is defined %}
    {{ form_row(form.contracts.company) }}
{% endif %}

{{ form_row(form.contracts.startDate) }}
{{ form_row(form.contracts.endDate) }}
{{ form_row(form.contracts.hourlyRate) }}
{{ form_row(form.contracts.isInsertion) }}


<script>
    $( document ).ready(function() {
        $('#employe_contract_contracts_type').on('change', function() {
            $.ajax({
                url : $form.attr('action'),
                type: $form.attr('method'),
                data : data,
                success: function(html) {
                    console.log(html);
                    console.log($(html));
                    $('#add_employe_step_3_container').replaceWith($(html).find('#add_employe_step_3_container'));
                }
            });

        });
    });
</script>

  1. Question / Problem :

My problem is that I'm not sure if CraueFlow is meant to work in the way I'm using it for FormEvent and I didnt find any documentation.

When I'm trying the way I just presented in this question I get a result from my Ajax call containing the first step of my Flow. The result is that $('#add_employe_step_3_container').replaceWith($(html).find('#add_employe_step_3_container')); does not load anything in my container because it is not found.

When I go to the next step and then come back to the third, the new company field appears in my step 3 because the global form seems to be updated and the formEvent has worked correctly.

Any ideas or examples ?


Solution

  • To refresh the form and reload errors and controls I just had to render the form without calling $flow->nextStep()

    /**
     * @Route("/user/add", name="user_add")
     */
    public function add(Request $request, UserPasswordEncoderInterface $encoder, UniquePasswordResetToken $uniqueToken){
    
        $employe = new Employe();
        $flow = $this->addUserFlow;
        $flow->bind($employe);
    
        // form of the current step
        $form = $flow->createForm();
        if ($flow->isValid($form)) {
            $flow->saveCurrentStepData($form);
            if ($request->isXmlHttpRequest()) {
                $form = $flow->createForm();
            } else {
    
                if ($flow->nextStep()) {
                    // form for the next step
                    $form = $flow->createForm();
                } else {
    
    
                    //Persist user
                    $this->manager->persist($employe);
                    $this->manager->flush();
    
                    //Persist change password user token
                    $this->manager->persist($uniqueToken->getToken($employe));
                    $this->manager->flush();
    
                    
                    //Reset flow to create new user
                    $flow->reset();
    
                    //Redirect to new user detail page
                    return $this->redirect($this->generateUrl('user_edit_personnal_info', array('id' => $employe->getId())));
                }
            }
        }
    
        return $this->render('app/users/users/add.html.twig', [
            'form' => $form->createView(),
            'flow' => $flow,
            'employe' => $employe
        ]);
    }