This is a fork of the package craue/CraueFormFlowBundle version 3.7.0. You can check it to know all the features of this package. In this fork i focus on the usage and installation on Symfony 7.
⚠️ Note: This fork doesn't support a Symfony version prior to 6.0.
Follow this guide for the installation.
This section shows how to create a 3-step form flow for a user. The package provides 03 approaches but i will focus on one: One form type for the entire flow. This approach makes it easy to turn an existing (common) form into a form flow. We will use this FormType:
// File: src/Form/UserType.php
<?php
// Imports...
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('email')
->add('first_name', TextType::class, [
'attr' => [
'placeholder' => 'Enter your first name'
],
])
->add('last_name')
->add('phone_number')
->add('professional_qualification')
->add('gender', EnumType::class, [
'class' => Gender::class,
'mapped' => false,
'expanded' => true,
'label' => 'gender',
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}
// src/Form/UserFlow.php
<?php
namespace App\Form;
use Asmitta\FormFlowBundle\Form\FormFlow;
class UserFlow extends FormFlow
{
protected function loadStepsConfig(): array
{
return [
[
'label' => 'Name',
'form_type' => UserType::class,
],
[
'label' => 'Contact',
'form_type' => UserType::class,
],
[
'label' => 'Profession',
'form_type' => UserType::class,
],
];
}
}
There is an option called flow_step
you can use to decide which fields will be added to the form
according to the step to render.
// UserType class
...
public function buildForm(FormBuilderInterface $builder, array $options): void
{
switch ($options['flow_step']) {
case 1:
$builder
->add('first_name', TextType::class, [
'attr' => [
'placeholder' => 'Enter your first name'
],
])
->add('last_name');
break;
case 2:
$builder
->add('email')
->add('phone_number');
break;
case 3:
$builder
->add('professional_qualification')
->add('gender', EnumType::class, [
'class' => Gender::class,
'mapped' => false,
'expanded' => true,
'label' => 'gender',
]);
break;
}
}
# config/services.yaml
services:
App\Form\userFlow:
parent: asmitta.form.flow
// in src/Controller/SomeController.php
final class SomeController extends AbstractController
{
private $flow;
public function __construct(UserFlow $flow)
{
$this->flow = $flow;
}
public function someMethod(): Response
{
$user = new User();
// Get existing flow data from session if it exists
$this->flow->bind($user);
$form = $this->flow->createForm();
if ($this->flow->isValid($form)) {
$this->flow->saveCurrentStepData($form); // Save data in session
$user = $form->getData();
if ($this->flow->nextStep()) {
$form = $this->flow->createForm(); // Go to the next step
} else {
// Persist data here
$this->flow->reset(); // remove all data from the session
return $this->redirectToRoute('some_route'); // redirect when done
}
}
return $this->render(
'custom_template.html.twig',
[
'form' => $form->createView(),
'flow' => $this->flow,
],
);
}
}
You only need one template for a flow.
The instance of your flow class is passed to the template in a variable called flow
so you can use it to render the
form according to the current step.
{# in src/templates/custom_template.html.twig #}
<div>
{{ form_start(form, {attr: {class: 'needs-validation', novalidate: ''}}) }}
{{ form_errors(form) }}
{{ form_rest(form) }}
<div class="mt-5">
{% include '@AsmittaFormFlow/FormFlow/buttons.html.twig' %}
</div>
{{ form_end(form) }}
</div>
You can customize the default button look by using these variables to add one or more CSS classes to them:
asmitta_formflow_button_class_last
will apply either to the next or finish buttonasmitta_formflow_button_class_finish
will specifically apply to the finish buttonasmitta_formflow_button_class_next
will specifically apply to the next buttonasmitta_formflow_button_class_back
will apply to the back buttonasmitta_formflow_button_class_reset
will apply to the reset button
Example with Bootstrap button classes:
{% include '@AsmittaFormFlow/FormFlow/buttons.html.twig' with {
asmitta_formflow_button_class_last: 'btn btn-primary',
asmitta_formflow_button_class_back: 'btn',
asmitta_formflow_button_class_reset: 'btn btn-warning',
} %}
In the same way you can customize the button labels:
asmitta_formflow_button_label_last
for either the next or finish buttonasmitta_formflow_button_label_finish
for the finish buttonasmitta_formflow_button_label_next
for the next buttonasmitta_formflow_button_label_back
for the back buttonasmitta_formflow_button_label_reset
for the reset button
Example:
{% include '@AsmittaFormFlow/FormFlow/buttons.html.twig' with {
asmitta_formflow_button_label_finish: 'Submit',
asmitta_formflow_button_label_reset: 'Reset the flow',
} %}
You can also remove the reset button by setting asmitta_formflow_button_render_reset
to false
.
The buttons are displayed in a div
with the class asmitta_formflow_buttons. You can override its rules in your CSS file.
/* Default*/
.asmitta_formflow_buttons {
overflow: hidden;
}
If you have unmapped fields in your form, you can handle them at the end of the form fill.
In our previous example, assuming the first_name
is unmapped we'll need to handle it ourself.
// In our controller
...
if ($this->flow->isValid($form)) {
$this->flow->saveCurrentStepData($form);
if ($this->flow->nextStep()) {
$form = $this->flow->createForm();
} else {
$stepNumber = 1; // It is the step where 'first_name' field is displayed
$user->setFirstName($this->flow->getStepData($stepNumber)['first_name']);
// Persist data here
$this->flow->reset();
return $this->redirectToRoute('some_route'); // redirect when done
}
}
...
Working with Symfony Turbo the flow in the view might not work correctly. You should disable it on that view or send a specific code with the controller.
- Disabling Turbo: o disable Turbo on a specific page in Symfony, you can use the
data-turbo
attribute. For example:
<div data-turbo="false">
<!-- Content that should not use Turbo -->
<!-- Insert your form here -->
</div>
- Sending a http code with the controller:
// Your Controller method
return $this->render(
'custom_template.html.twig',
[
'form' => $form->createView(),
'flow' => $this->flow,
],
new Response(status: Response::HTTP_UNPROCESSABLE_ENTITY)
// Without this Symfony Turbo will not load the new steps, it will stuck on the first step.
);