-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Form] Add MultiStepType
#59548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Form] Add MultiStepType
#59548
Changes from all commits
e83ec2a
3f74429
bc77606
92c89e6
1befa38
d6e3215
5771ee6
21780e4
9025a65
865288b
a71ee28
a23b651
5dba331
8784a81
0f0b645
4f677b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,11 @@ | ||
CHANGELOG | ||
========= | ||
|
||
7.3 | ||
--- | ||
|
||
* Add `MultiStepType` to create multistep forms | ||
|
||
7.2 | ||
--- | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Form\Extension\Core\Type; | ||
|
||
use Symfony\Component\Form\AbstractType; | ||
use Symfony\Component\Form\FormBuilderInterface; | ||
use Symfony\Component\Form\FormInterface; | ||
use Symfony\Component\Form\FormView; | ||
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; | ||
use Symfony\Component\OptionsResolver\Options; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
|
||
/** | ||
* @author Silas Joisten <silasjoisten@proton.me> | ||
* @author Patrick Reimers <preimers@pm.me> | ||
* @author Jules Pietri <jules@heahprod.com> | ||
*/ | ||
final class MultiStepType extends AbstractType | ||
{ | ||
public function configureOptions(OptionsResolver $resolver): void | ||
{ | ||
$resolver | ||
->setRequired('steps') | ||
->setAllowedTypes('steps', 'array') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should all current_step / next_step etc allow callable ? That would ease integration with Stepper or Navigator or any future WizardStepPathFinder-like.. wdyt ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea but i dont know what i should do here. Maybe i dont fully understand the approach |
||
->setAllowedValues('steps', static function (array $steps): bool { | ||
foreach ($steps as $key => $step) { | ||
if (!\is_string($key)) { | ||
return false; | ||
} | ||
|
||
if ((!\is_string($step) || !is_subclass_of($step, AbstractType::class)) && !\is_callable($step)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A Also, I have the feeling that parenthesis should be added here (mixing |
||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
}) | ||
->setRequired('current_step') | ||
->setAllowedTypes('current_step', 'string') | ||
->setNormalizer('current_step', static function (Options $options, string $value): string { | ||
if (!\array_key_exists($value, $options['steps'])) { | ||
throw new InvalidOptionsException(\sprintf('The current step "%s" does not exist.', $value)); | ||
} | ||
|
||
return $value; | ||
}) | ||
->setDefault('current_step', static function (Options $options): string { | ||
/** @var string $firstStep */ | ||
$firstStep = array_key_first($options['steps']); | ||
|
||
return $firstStep; | ||
}) | ||
->setRequired('next_step') | ||
->setAllowedTypes('next_step', ['string', 'null']) | ||
->setDefault('next_step', function (Options $options): ?string { | ||
return array_keys($options['steps'])[$this->currentStepIndex($options['current_step'], $options['steps']) + 1] ?? null; | ||
}) | ||
->setNormalizer('next_step', static function (Options $options, ?string $value): ?string { | ||
if (null === $value) { | ||
return null; | ||
} | ||
|
||
if (!\array_key_exists($value, $options['steps'])) { | ||
throw new InvalidOptionsException(\sprintf('The next step "%s" does not exist.', $value)); | ||
} | ||
|
||
return $value; | ||
}) | ||
->setRequired('previous_step') | ||
->setAllowedTypes('previous_step', ['string', 'null']) | ||
->setDefault('previous_step', function (Options $options): ?string { | ||
return array_keys($options['steps'])[$this->currentStepIndex($options['current_step'], $options['steps']) - 1] ?? null; | ||
}) | ||
->setNormalizer('previous_step', static function (Options $options, ?string $value): ?string { | ||
if (null === $value) { | ||
return null; | ||
} | ||
|
||
if (!\array_key_exists($value, $options['steps'])) { | ||
throw new InvalidOptionsException(\sprintf('The previous step "%s" does not exist.', $value)); | ||
} | ||
|
||
return $value; | ||
}); | ||
|
||
$resolver->setDefaults([ | ||
'hide_back_button_on_first_step' => false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. display_ with default true instead of hide_ default false? |
||
'button_back_options' => [ | ||
'label' => 'Back', | ||
], | ||
'button_next_options' => [ | ||
'label' => 'Next', | ||
], | ||
'button_submit_options' => [ | ||
'label' => 'Finish', | ||
], | ||
]); | ||
|
||
$resolver->setAllowedTypes('hide_back_button_on_first_step', 'bool'); | ||
$resolver->setAllowedTypes('button_back_options', 'array'); | ||
$resolver->setAllowedTypes('button_submit_options', 'array'); | ||
} | ||
|
||
public function buildForm(FormBuilderInterface $builder, array $options): void | ||
{ | ||
$currentStep = $options['steps'][$options['current_step']]; | ||
silasjoisten marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if (\is_callable($currentStep)) { | ||
$currentStep($builder, $options); | ||
} elseif (\is_string($currentStep)) { | ||
$builder->add($options['current_step'], $currentStep); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the step type may require specific options that differ from those in the root form. we need to consider this, as it's currently a limitation |
||
} | ||
|
||
$builder->add('back', SubmitType::class, [ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not a fan of buttons within form type objects. I prefer their integration into the template. |
||
'disabled' => $this->isFirstStep($options['current_step'], $options['steps']), | ||
'validate' => false, | ||
...$options['button_back_options'], | ||
]); | ||
|
||
if ($this->isFirstStep($options['current_step'], $options['steps']) && true === $options['hide_back_button_on_first_step']) { | ||
$builder->remove('back'); | ||
} | ||
|
||
$builder->add('submit', SubmitType::class, $this->isLastStep($options['current_step'], $options['steps']) ? $options['button_submit_options'] : $options['button_next_options']); | ||
} | ||
|
||
public function buildView(FormView $view, FormInterface $form, array $options): void | ||
{ | ||
$view->vars['current_step'] = $options['current_step']; | ||
$view->vars['steps'] = array_keys($options['steps']); | ||
yceruto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$view->vars['total_steps_count'] = \count($options['steps']); | ||
$view->vars['current_step_number'] = $this->currentStepIndex($options['current_step'], $options['steps']) + 1; | ||
$view->vars['is_first_step'] = $this->isFirstStep($options['current_step'], $options['steps']); | ||
$view->vars['is_last_step'] = $this->isLastStep($options['current_step'], $options['steps']); | ||
$view->vars['previous_step'] = $options['previous_step']; | ||
$view->vars['next_step'] = $options['next_step']; | ||
} | ||
|
||
/** | ||
* @param array<string, mixed> $steps | ||
*/ | ||
private function currentStepIndex(string $currentStep, array $steps): int | ||
{ | ||
/** @var int $currentStep */ | ||
$currentStep = array_search($currentStep, array_keys($steps), true); | ||
|
||
return $currentStep; | ||
} | ||
|
||
/** | ||
* @param array<string, mixed> $steps | ||
*/ | ||
private function isLastStep(string $currentStep, array $steps): bool | ||
{ | ||
return array_key_last(array_keys($steps)) === $this->currentStepIndex($currentStep, $steps); | ||
} | ||
|
||
/** | ||
* @param array<string, mixed> $steps | ||
*/ | ||
private function isFirstStep(string $currentStep, array $steps): bool | ||
{ | ||
return 0 === $this->currentStepIndex($currentStep, $steps); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.