Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

[Form] AddMultiStepType#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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Closed

Conversation

silasjoisten
Copy link
Contributor

@silasjoistensilasjoisten commentedJan 18, 2025
edited
Loading

QA
Branch?7.3
Bug fix?no
New feature?yes
Deprecations?no
Issues-
LicenseMIT

This PR introduces theMultiStepType form type, which allows for the creation of form flows. The idea for this form type was initially proposed in aPR for Symfony UX, where it was suggested that the type would be better suited within the Symfony Form component.

But this form works also without Symfony UX.

Usage:

declare(strict_types=1);namespaceApp\Form;useSymfony\Component\Form\Extension\Core\Type\MultiStepType;useSymfony\Component\Form\AbstractType;useSymfony\Component\Form\Extension\Core\Type\CheckboxType;useSymfony\Component\Form\Extension\Core\Type\NumberType;useSymfony\Component\Form\Extension\Core\Type\TextType;useSymfony\Component\Form\FormBuilderInterface;useSymfony\Component\OptionsResolver\OptionsResolver;useSymfony\Component\Validator\Constraints\IsTrue;useSymfony\Component\Validator\Constraints\NotBlank;useApp\Form\AuthorType;finalclass MyFancyWizardTypeextends AbstractType{/**     * @return class-string<AbstractType>     */publicfunctiongetParent():string    {return MultiStepType::class;    }publicfunctionconfigureOptions(OptionsResolver$resolver):void    {$resolver->setDefaults(['steps' => ['general' =>staticfunction (FormBuilderInterface$builder,array$options):void {$builder                        ->add('age', NumberType::class, ['label' =>'Age','constraints' => [newNotBlank(),                            ],                        ]);if ($options['customFlag'] ==='yes') {$builder->add('name', TextType::class, ['label' =>'Name','constraints' => [newNotBlank(),                            ],                        ]);                    }else {$builder->add('notname', TextType::class, ['label' =>'Not Name','constraints' => [newNotBlank(),                            ],                        ]);                    }                },'contact' =>staticfunction (FormBuilderInterface$builder):void {$builder                        ->add('email', TextType::class, ['label' =>'E-Mail','constraints' => [newNotBlank(),                            ],                        ])                        ->add('newsletter', CheckboxType::class, ['label' =>'Newsletter','constraints' => [newIsTrue(),                            ],                        ]);                },'author' => AuthorType::class,            ],        ]);    }}

In the controller (this is onlyone possible usage of the form in order to persist the current step and the data):

<?phpdeclare(strict_types=1);namespaceApp\Controller;useApp\Form\MyFancyMultiStepType;useSymfony\Bundle\FrameworkBundle\Controller\AbstractController;useSymfony\Component\HttpFoundation\Request;useSymfony\Component\HttpFoundation\Response;useSymfony\Component\Routing\Attribute\Route;finalclass HelloFormControllerextends AbstractController{    #[Route(path:'/hello-form', name:'hello_form', methods: ['GET','POST'])]publicfunctionindex(Request$request):Response    {$session =$request->getSession();$currentStep =$session->get('current_step','general') ??'general';$form =$this->createForm(MyFancyMultiStepType::class,$session->get($currentStep, []), ['current_step' =>$currentStep,        ]);$session->set('current_step',$form->getConfig()->getOption('current_step'));$form->handleRequest($request);if ($form->isSubmitted()) {if ($form->has('back') &&$form->get('back')->isClicked()) {$session->set('current_step',$form->getConfig()->getOption('previous_step'));return$this->redirectToRoute('hello_form');            }if ($form->get('submit')->isClicked() &&$form->isValid()) {$session->set($currentStep,$form->getData());$session->set('current_step',$form->getConfig()->getOption('next_step'));if (null !==$form->getConfig()->getOption('next_step')) {return$this->redirectToRoute('hello_form');                }$data = [];foreach (\array_keys($form->getConfig()->getOption('steps'))as$name) {$data[$name] =$session->get($name, []);$session->remove($name);                }dump($data);return$this->redirectToRoute('hello_form');            }        }return$this->render('hello_form/template.html.twig', ['form' =>$form->createView(),        ]);    }}

And Rendering:

<divclass="max-w-6xl mx-auto">    <divclass="my-12">        {%forstepinform.vars.steps %}            <spanclass="{{ html_classes({'font-bold':step ==form.vars.current_step }) }}">{{step }}</span> {%ifnotloop.last %} - {%endif %}        {%endfor %}    </div>    {{ form_start(form) }}    {{ form_end(form) }}    <divclass="mt-12">        {{form.vars.current_step_number }} / {{form.vars.total_steps_count }}    </div></div>

Look and feel!

CleanShot 2025-01-19 at 14 44 00

javiereguiluz, ahmedyakoubi, seb-jean, daFish, Kocal, yceruto, skmedix, Crovitche-1623, PReimers, manuelderuiter, and 10 more reacted with thumbs up emojijaviereguiluz, seb-jean, daFish, skmedix, PReimers, kaznovac, and andreybolonin reacted with hooray emojijaviereguiluz, seb-jean, PReimers, kaznovac, tito10047, and haithem-rihane reacted with heart emojijaviereguiluz, seb-jean, PReimers, kaznovac, willemverspyck, and sstok reacted with rocket emoji
@silasjoistensilasjoisten changed the titleEnhancement: AddsMultiStepType[Form] Enhancement: AddsMultiStepTypeJan 18, 2025
@silasjoistensilasjoisten changed the title[Form] Enhancement: AddsMultiStepType[Form] AddMultiStepTypeJan 18, 2025
Copy link
Member

@ycerutoyceruto left a comment
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Interesting idea!

I wonder if we could create a default theme for this form type that would (at least):

  • render the current step form (this is already the case but might require some tweaks for the next point below)
  • render the back/next "submit" buttons based on the "step number" (which is currently missing but can be calculated easily using the provided options)

While data storage and form handling are beyond the scope of this type, I believe having that default theme would be great.

@carsonbotcarsonbot changed the title[Form] AddMultiStepTypeAddMultiStepTypeJan 18, 2025
@silasjoisten
Copy link
ContributorAuthor

silasjoisten commentedJan 18, 2025
edited
Loading

Hey thanks for the review!

render the back/next "submit" buttons based on the "step number" (which is currently missing but can be calculated easily using the provided options)

I tried today to find a solution which is nice but i did not succeed. If we have a next and a previous button there would be also the need of disabling the previous button when the current step number is 0 and maybe change the label of the next button when the current step number is the last step. All this requires some sort of wrapper class including a storage. (Which will be handled in UX) if you got any idea of how to achieve that i'd very happy!

I added some more helpers in view vars which makes rendering easier and also i added some options in order to allow navigate through steps. I also updated this PR description with an example of how to use the form in a controller.

@carsonbotcarsonbot changed the titleAddMultiStepType[Form] AddMultiStepTypeJan 19, 2025
Copy link
Member

@ycerutoyceruto left a comment
edited
Loading

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I think the Steps and Navigator elements should be designed as two distinct types (even if they are related). This would make it easier for users to extend/customize each one independently.

smnandre reacted with thumbs up emoji
{
$resolver
->setRequired('steps')
->setAllowedTypes('steps', 'array')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

array ->(string|callable)[] this is a fresh feature already merged in 7.3

silasjoisten reacted with heart emoji
Copy link
Member

Choose a reason for hiding this comment

The 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 ?

Copy link
ContributorAuthor

Choose a reason for hiding this comment

The 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

@yceruto
Copy link
Member

After reviewing the PR description again following your latest update, it might be helpful to include an example with a DTO (+ validator constraints bound to the underlying object) as this aligns with the recommended approach for working with forms.

I like the idea and how we can configure the steps, but handling it from the controller still feels a bit complex IMO.

@silasjoisten
Copy link
ContributorAuthor

After reviewing the PR description again following your latest update, it might be helpful to include an example with a DTO (+ validator constraints bound to the underlying object) as this aligns with the recommended approach for working with forms.

Sure i can add that. Thats just gonna be a fee lines of code.

I like the idea and how we can configure the steps, but handling it from the controller still feels a bit complex IMO.

Well but all of that is implementation detail. Like which storage to use which steps to skip or add to your multistep type. Without an implementation of a storage its not possible to enhance the DX. Mainly we should not forget that the main reason for this PR is to have this available for a nice LiveComponent in symfony UX.

If you have any better idea of how to increase the developer experience feel free to tell. I honestly ran out of ideas because i tried a lot of different things today. And i always came to the same conclusion... storage, storage, storage 😅

@smnandre
Copy link
Member

Would this require a lot of work to implement the following (not in this PR of course)

  • serialize / encode the data from previous steps
  • sign it
  • add a hidden field / textarea

Or is this "one" of the storages we're talking about ? Will there be any interface for the "storage" in symfony/form .. or we let entirely people implementing from scratch this part ?

@PReimers
Copy link
Contributor

PReimers commentedJan 19, 2025
edited
Loading

Will there be any interface for the "storage" in symfony/form .. or we let entirely people implementing from scratch this part ?

We "started" with a storage imterface (and a session storage) when we build it for UX.
https://github.com/symfony/ux/blob/4c6a06576d30caf1b96a0ba2a66d0f896f767edb/src/LiveComponent/src/Storage/StorageInterface.php
https://github.com/symfony/ux/blob/4c6a06576d30caf1b96a0ba2a66d0f896f767edb/src/LiveComponent/src/Storage/SessionStorage.php

But in the meantime we figured Symfony has no place for a storage interface (except if we would create something like a storage-contract. But a storage-contract would only provide one interface, implementation like a SessionStorage/FilesystemStorage/RedisStorage/... would need to go into different components (SessionStorage in Framework? FilesystemStore in Filesystem? RedisStorage in ...)

Or are we open to add "storage" into Form?

silasjoisten reacted with thumbs up emoji

@yceruto
Copy link
Member

Without an implementation of a storage its not possible to enhance the DX

What about adding a newFormFlow utility + a newDataStorageInterface to implement that abstraction?

Mainly we should not forget that the main reason for this PR is to have this available for a nice LiveComponent in symfony UX.

I think you raise an important point here. Introducing this type in the Symfony repo would mean designing it in a way that isn't strictly tied to the UX package, as not all users may be using UX packages like we do. It's essential to ensure that it works well for everyone.

silasjoisten and chalasr reacted with thumbs up emoji

@PReimers
Copy link
Contributor

Would this require a lot of work to implement the following (not in this PR of course)

* serialize / encode the data from previous steps* sign it* add a hidden field / textarea

We were thinking about implementing a hidden field containing the current state, but when the page is reloaded we'll lose the current state.

In our current example implementaion we're using the session, this is persistent across reloads (until the session is killed).
Would we need to serialize/deserialize and/or sign the data in that case?

@silasjoisten
Copy link
ContributorAuthor

What about adding a newFormFlow utility + a newDataStorageInterface to implement that abstraction?

Yes we could do that. As we already have the Interface. How do we do the implementation? Shall i require http foundation for the session?

I think you raise an important point here. Introducing this type in the Symfony repo would mean designing it in a way that isn't strictly tied to the UX package, as not all users may be using UX packages like we do. It's essential to ensure that it works well for everyone.

I agree on that. But isn't it usable without symfony ux yet? I mean it might be a bit more code to write but it would be already usable.

Regarding the storage where should i put it? Can you maybe help me on this one?

PReimers reacted with thumbs up emoji

@smnandre
Copy link
Member

I would not be shocked if a "FormStepPersisterInterface" or "Loader" or something like this was added into Form 🤷

Like ChoiceLoaderInterface maybe ?

Or simply document the events and to plug itself to store the data.

We also can consider there is only one entity saved, partially at each step, and then nothing is really needed here ?

@smnandre
Copy link
Member

In our current example implementaion we're using the session, this is persistent across reloads (until the session is killed).
Would we need to serialize/deserialize and/or sign the data in that case?

I guess not, indeed! 👍

(documentation will maybe just need a warning regarding funnel forms with login or register in the way, as session is often reset then)

PReimers reacted with thumbs up emoji

@yceruto
Copy link
Member

Yes we could do that. As we already have the Interface. How do we do the implementation? Shall i require http foundation for the session?

We shouldn't require the http foundation directly, take a look atRequestHandlerInterface mechanism (and their subclasses).

Regarding the storage where should i put it? Can you maybe help me on this one?

Let me play a bit with this proposal and I'll back next week with more details about it.

PReimers and silasjoisten reacted with thumbs up emoji

@silasjoisten
Copy link
ContributorAuthor

We also can consider there is only one entity saved, partially at each step, and then nothing is really needed here ?

If you concider that yes then the implementation would be Userland

@yceruto
Copy link
Member

For referencehttps://github.com/craue/CraueFormFlowBundle. I've used this bundle in several projects and found it suitable for most advanced cases. It's worth a look and a ping to@craue who has expertise on this topic.

bretrzaun, AlexiZ, and zmitic reacted with thumbs up emoji

@stof
Copy link
Member

@silasjoisten I suggest updating your example code to remove the usage ofOskarStark\Symfony\Http\Responder. An example using core APIs is more useful as the community will be familiar with them (and will help the documentation team)

silasjoisten and OskarStark reacted with thumbs up emoji

@silasjoisten
Copy link
ContributorAuthor

silasjoisten commentedJan 21, 2025
edited
Loading

For referencehttps://github.com/craue/CraueFormFlowBundle. I've used this bundle in several projects and found it suitable for most advanced cases. It's worth a look and a ping to@craue who has expertise on this topic.

Yea i have worked with it as well and i did not like the DX in it. It felt quite old and i mean its a common problem why shouldn't it be part of Symfony itself. Even if its just a simple form flow without skipping things.

For some cases yes you need a more complex form flow. but sometimes you want to have it in order to have a nice User experience in you Application.

@PReimers
Copy link
Contributor

What is the current state of this PR?
Do we need to do something, or are we waiting for something?

@yceruto
Copy link
Member

yceruto commentedFeb 8, 2025
edited
Loading

I tested the proposal as it is, and it feels like too much responsibility for the user to handle (in the current state). I'm not referring to the form step definition, which is already simple, but to the navigation and data storage part across steps. IMO, there should be a default implementation that handles that for us, not a UX but a Symfony one, flexible enough to be used with or without UX capabilities.

This is my current expectation for this feature (https://gist.github.com/yceruto/0fe65c8669016fe48f24c4e047ce7fb1):

  • it should work with a DTO (including the current step property)

  • the step validation should follow thevalidation_groups approach with constraints defined in the DTO, e.g.

    $resolver->setDefault('validation_groups',function (Options$options) {return ['Default',$options['current_step']];});
  • default navigation buttons (back, next, submit) and an action-based navigation handling approach (rather than checking by the button name). For example, I might add a new "skip" button with a "next" action in one step, which skips validations.

  • a customizable data storage strategy (probably through a form option) to persist the data between steps. The session strategy should be the default.

my two cents :) still looking forward to this feature 💘

silasjoisten, KristianH, and PReimers reacted with thumbs up emojisilasjoisten, PReimers, and chalasr reacted with heart emojisilasjoisten and 94noni reacted with rocket emoji

@silasjoisten
Copy link
ContributorAuthor

Nicee gist you made there! I focus ghis weekend on this Pr. And try to make it as you expect :) i like your idea very much!

smnandre and yceruto reacted with heart emoji

if (\is_callable($currentStep)) {
$currentStep($builder, $options);
} elseif (\is_string($currentStep)) {
$builder->add($options['current_step'], $currentStep);
Copy link
Member

Choose a reason for hiding this comment

The 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

Spomky reacted with thumbs up emoji
@94noni
Copy link
Contributor

Comming from the initial UX repo PR :) nice one

If something like this land on sf/form I think it needs storage included otherwise it may be « hard/prone to error » to implement/handle and may lead to devland issues no?

});

$resolver->setDefaults([
'hide_back_button_on_first_step' => false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

display_ with default true instead of hide_ default false?
Seems easier to read the positive way not the négative

OskarStark, silasjoisten, and Spomky reacted with thumbs up emoji
Copy link
Contributor

@SpomkySpomky left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Many thanks for this PR.
I find the idea really very interesting. This type of need is common in applications.
However I have some questions/remarks.

return false;
}

if ((!\is_string($step) || !is_subclass_of($step, AbstractType::class)) && !\is_callable($step)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

A$step object that implementsFormTypeInterface should be allowed as well

Also, I have the feeling that parenthesis should be added here (mixing|| and&&).

$builder->add($options['current_step'], $currentStep);
}

$builder->add('back', SubmitType::class, [
Copy link
Contributor

@SpomkySpomkyMar 4, 2025
edited
Loading

Choose a reason for hiding this comment

The 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.
There should be a way for the template to know when each button has to be displayed or not e.g. with the help of theisFirstStep orisLastStep methods.
Also, this will remove the need of thehide_back_button_on_first_step,button_back_options,button_next_options andbutton_submit_options options. WDYT?

@yceruto
Copy link
Member

Hi there! I'll get back to this topic next week with a complete alternative proposal that addresses the main implementation concerns (this one:#59548 (comment) and others mentioned).

I can't wait to share it with all of you!

Spomky, OskarStark, and skmedix reacted with thumbs up emojismnandre, chalasr, OskarStark, and skmedix reacted with hooray emojichalasr, OskarStark, mtarld, and skmedix reacted with rocket emoji

@yceruto
Copy link
Member

Here we go#60212 !

OskarStark and silasjoisten reacted with rocket emoji

@silasjoisten
Copy link
ContributorAuthor

I will close this one thank you@yceruto

yceruto reacted with heart emoji

@silasjoistensilasjoisten deleted the feature/multi-step-type branchApril 14, 2025 19:57
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@SpomkySpomkySpomky left review comments

@94noni94noni94noni left review comments

@smnandresmnandresmnandre left review comments

@ycerutoycerutoyceruto requested changes

@xabbuhxabbuhAwaiting requested review from xabbuhxabbuh is a code owner

Assignees
No one assigned
Projects
None yet
Milestone
7.3
Development

Successfully merging this pull request may close these issues.

10 participants
@silasjoisten@dsentker@yceruto@smnandre@PReimers@stof@94noni@Spomky@GromNaN@carsonbot

[8]ページ先頭

©2009-2025 Movatter.jp