Skip to content

[Form] Support "allow_add"/"allow_delete" in ChoiceType #9310

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

Closed
webmozart opened this issue Oct 16, 2013 · 22 comments
Closed

[Form] Support "allow_add"/"allow_delete" in ChoiceType #9310

webmozart opened this issue Oct 16, 2013 · 22 comments

Comments

@webmozart
Copy link
Contributor

ChoiceType should support the options "allow_add" and "allow_delete" in the same fashion as CollectionType does.

@HeahDude
Copy link
Contributor

A big 👍 for allow_add as I said in #6602 (comment), but I at this time I was not aware of this issue.

allow_add would basically allow to accept a submitted choice which doesn't belong to initial choices, but I don't get what would be the benefit of allow_delete in ChoiceType ?

@stof
Copy link
Member

stof commented Mar 1, 2016

How would the added choice value be transformed to a model value though ?

@HeahDude
Copy link
Contributor

HeahDude commented Mar 1, 2016

@stof, the same way you actually deal with empty_data. @webmozart already suggested to rename it to something like create_data in #5939.

@webmozart
Copy link
Contributor Author

That could work. However, automatic construction using the data_class and population using the data mapper should work as well as a default mechanism, just as it does everywhere else.

This might be a bit complicated to implement though.

@qferr
Copy link

qferr commented Sep 30, 2016

Any news about this feature ?

@HeahDude
Copy link
Contributor

@qferr It's very complicated. See https://speakerdeck.com/heahdude/symfony-forms-use-cases-and-optimization for an example of a custom entity type handling creation by extending the ChoiceType.

I'll try to work on a global implementation for 3.3 :)

@Shoplifter
Copy link
Contributor

Is this still a planned feature? Would be great for e.g. tagging, etc. and js tools like Selectize.js or similar others could be deployed a lot easier

@xabbuh
Copy link
Member

xabbuh commented Sep 1, 2018

We do not have any "planned features" in Symfony. Each issue labelled as a feature is just a wish of someone that could happen if someone volunteers to implement it. It seems that until today nobody needed this feature so badly that they created a pull request implementing it.

@ro0NL
Copy link
Contributor

ro0NL commented Oct 14, 2018

allow_add is implicitly allowed when using a choice_loader, except the value is not passed back to the view as a choice only in {{ value }}. That's "good enough" for me.

Adding allow_add only for choices is indeed a bit complex and IMO less intuitive. At least on the technical side.

I propose to close.

See also #23679 (i think we should close that one as well, and keep it as a feature)

@pcP1r4t3
Copy link

Personnally I consider it a very important feature for ChoiceType and EntityType.

@ro0NL
Copy link
Contributor

ro0NL commented Oct 17, 2018

but if it can be solved using choice loaders already (e.g. in userland) it's not worth it IMO. we should investigate it :)

@Daisuke-sama
Copy link

For me choice_loader is not working, because it should be set in the configureOptions(), but at the same time I already need yo use that options resolved to generate choices.
I am creating a new FormType , which parent is ChoiceType, and I am not building the form, I am just using getParent() and configureOptions(). Somewhere I even do , which is a trick for frontend generation, but doesn't avoid TransformationFailedException: message: "The choice "xxx" does not exist or is not unique"

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        foreach ($this->getRange($options['vehicle_type']) as $value) {
            $view->vars['choices'][] = new ChoiceView($value, $value, $value);
        }
    }

@xabbuh
Copy link
Member

xabbuh commented Jul 21, 2019

@Daisuke-sama Can you give a more concrete example of what you want to achieve and isn't possible right now?

@nzurita
Copy link

nzurita commented Jul 29, 2019

@xabbuh in this issue, marked as duplicated of this one, I give an specific example, I don't know if this is what you ask for.

@xabbuh
Copy link
Member

xabbuh commented Aug 6, 2019

@nzurita Sorry for the late reply. If I do not miss anything, this looks like a good example where one would simply populate (i.e. add) the choice type in a PRE_SET_DATA and PRE_SUBMIT event listener.

@nzurita
Copy link

nzurita commented Oct 15, 2019

Hello @xabbuh , sorry for the late reply, I've been taken by other projects. What I wrote in that issue is that, when it comes to multilevel and dynamic select lists, for example, country > state/province > town, this event listener solution becomes quite unfriendly, and a pure ajax solution would be more desirable,

@Alsatian67
Copy link

I created a poor bundle to provide this feature for ChoiceType and EntityType.

It recreate the type taking the pre-set-dat or pre-submit data as choices. It doesn't cover all use case, and it would be better to have an official built-in "allow_add" option.

I understand, it would be difficult for the ChoiceType (model conversion). But for the EntityType it should be easier. And I think it is a very common task (in a database table, we often have thousands of entities, in this case it is often better to load them with Ajax calls, for example with the select2 ajax library).

Despite of the low quality of my bundle, I can see it was installed more than 1000 times on packagist, and I often see stackoverflow question asking how to implement something like that.

@Alsatian67
Copy link

A very typical example is to bind a EntityType with the select2 Javscript library.

Assuming you have an entity "City" binded with a cities table in the SQL database. This table has several millions of entries and we can't load all of them in the choices option. So we will start generating the entitytype with an emty choicelist.

Using the select2 library, on the client side we can do Ajax calls to search available cities depending on a searched string. Once selected, select2 will add them to the HTML select élément and set them selected.

The server will receive selected choices which were not in the initial choices. If there were any option "allow_add" it could simply add the selected entities to the availables choices if the submitted data can be converted innan entity.

@that-guy-iain
Copy link
Contributor

I've just had to deal with this so I figured I would chip in here. This got closed because there are ways of achieving the same goal currently. Ok, but I think we should look at the fact we're currently increasing complexity and in many cases, we're receiving no benefits. If I have a UI that is using a select for UI purposes and I want to dynamically add things to that select via user input, for whatever reason., I then need to have a workaround for the fact that the Symfony tool to help me with my form UI won't let me do that.

My choices as I see them now.

I can either add a new custom field type where I have a hidden text input and then a select UI that is not important for the form and then I dynamically add that. So now I've increased my code complexity by adding a new field type and also the JS logic so that when a select is changed the hidden field in my custom field type is populated. That's not a pleasant experience.

Or

I can add an event lister find the value in the data sent in the HTTP request, remove the choice field and re-add it with this new user provider option. Again, do-able but not pleasant.

For me, the issue is more that we're adding more code that is fundamentally a code smell. For no benefit to us other than we continue to be able to use Symfony Forms. I personally think having the ability to do workarounds shouldn't be a reason in itself not to add a feature.

Reading through the comments here it seems that the only issue here is not that actual feature but that no one has added it, but the pull request is labelled waiting for feedback, I personally wouldn't go implementing a feature that has an issue that is awaiting feedback. Maybe this is highlighting a possible improvement in Symfony issue workflow that could help with new people looking to contribute, maybe it would be an idea to add a new label "Waiting for a contributor" or something that tells people that this idea is acceptable and just needs someone to contribute a PR to implement the feature.

That being said, if I worked on this would it be accepted?

@StuggiBear
Copy link

StuggiBear commented Aug 24, 2022

I am having exactly the same issue and find it incredible that it is not being taken seriously!
My (simple) example is my symphony app that is basically my version of Discogs but also for movies, TV, etc. When I go to create a new TV show, I have a select2 field that allows me to select, if necessary, the name of the TV series it belongs to. If I select an existing series then upon saving the item, the ID of the selected series is written into the series field of the item.
However, If I type a series name in the select2 field for a series that does not yet exist, I want to be able to add that series to the series table on the fly and subsequently save it's id in the item - a little but like using an embedded form with CollectionType.
Surely this is not too much to ask and must be needed by so many people.

@Averor
Copy link

Averor commented Aug 4, 2023

@StuggiBear

[and yes, i know this thread is old ;)] Not sure if that's your case, but for some, below example may be enough (element presented as select field, submitted value will be dynamically added on PreSubmit event)
(note: createFormBuilder is of course Symfony\Bundle\FrameworkBundle\Controller\AbstractController shortcut method)

$form = $this->createFormBuilder()
    ->add(child: 'SomeSelectField', type: ChoiceType::class, options: [
        'choices' => [
            'choice1' => 'value1',
            'choice1' => 'value2',
            'choice1' => 'value3'
        ],
    ])
    ->add(child: 'save', type: SubmitType::class, options: [
        'label' => 'Try me'
    ])

    ->addEventListener(
        FormEvents::PRE_SUBMIT,
        function (PreSubmitEvent $event): void {

            // fetch submitted value
            $data = $event->getData()['SomeSelectField']; 

            $form = $event->getForm();

            // retrieve original select field options, so we won't need to repeat them
            $opts = $form->get('SomeSelectField')->getConfig()->getOptions();  

            // here we're adding our fetched submitted value to the list of select field options
            $opts['choices'][$data] = $data;

            // not sure if this is needed, but i like to leave it for clearity
            $form->remove('SomeSelectField');

            // add reconfigured (=with changed options) field
            $form->add(child: 'SomeSelectField', type: ChoiceType::class, options: $opts);
        }
    )

    ->getForm();

@fsevestre
Copy link
Contributor

Hello, the event system is working fine as long as you don't try to move the form field in his own custom type for reusability.
The POST_SET_DATA event will make an infinite loop while recreating the input (can be fixed with an option to listen the event only the first time). But then the PRE_SUBMIT will never have the new dynamic choices because of the ChoiceType own internal PRE_SUBMIT event listener which will remove unknown choices.

I replaced the event system by a choice loader implementation and it is working fine :

<?php

declare(strict_types=1);

namespace App\Observer\ProductLine\UI\Http\Web\Html\Form;

use App\Observer\ProductLine\Application\Query\ListAsSymfonyFormChoices\ListProductLinesAsSymfonyFormChoicesQuery;
use App\Observer\ProductLine\Application\Query\ListAsSymfonyFormChoices\ListProductLinesAsSymfonyFormChoicesViewModel;
use App\Observer\ProductLine\Domain\Entity\ValueObject\ProductLineId;
use App\Shared\Application\Bus\Query\QueryBus;
use App\Shared\Domain\Assert\Assert;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\RouterInterface;

class AutocompleteProductLineFormType extends AbstractType
{
    public function __construct(private QueryBus $queryBus, private RouterInterface $router)
    {
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'choice_loader' => new class($this->queryBus) extends AbstractChoiceLoader {
                /**
                 * @var ProductLineId[]
                 */
                private array $productLineIds = [];

                public function __construct(private readonly QueryBus $queryBus)
                {
                }

                public function loadChoicesForValues(array $values, callable $value = null): array
                {
                    $this->productLineIds = array_map(fn (string $id) => ProductLineId::fromInt((int) $id), $values);

                    return parent::loadChoicesForValues($values, $value);
                }

                public function loadValuesForChoices(array $choices, callable $value = null): array
                {
                    Assert::allIsInstanceOf($choices, ProductLineId::class);
                    $this->productLineIds = $choices;

                    return parent::loadValuesForChoices($choices, $value);
                }

                /**
                 * @return iterable<ProductLineId>
                 */
                protected function loadChoices(): iterable
                {
                    if ([] === $this->productLineIds) {
                        return [];
                    }

                    /** @var ListProductLinesAsSymfonyFormChoicesViewModel $choices */
                    $choices = $this->queryBus->ask(new ListProductLinesAsSymfonyFormChoicesQuery($this->productLineIds));

                    return $choices->choices;
                }
            },
            'choice_value' => function (ProductLineId $productLineId) {
                return $productLineId->value();
            },
        ]);
    }

    public function buildView(FormView $view, FormInterface $form, array $options): void
    {
        $view->vars['attr']['data-ajax-url'] = $this->router->generate('tom_select_product_line_list');
    }

    public function getParent(): string
    {
        return ChoiceType::class;
    }
}

On a side note, I you need to display multiple times the same form type (I have a primary and secondary select list with the same data), you will need to extend this class (not using the same form type, nor doing sub custom form type referring to it using getParent()). From what I have seen the data sent to the choice loader will be from last the select (as they references the same SF service). Doing multiple sub form types will separate the data in multiple SF services. (At least it worked for me this way)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests