SymfonyCasts / dynamic-forms

Add dynamic/dependent fields to Symfony forms
MIT License
84 stars 4 forks source link

This form should not contain extra fields. #23

Open leevigraham opened 2 months ago

leevigraham commented 2 months ago

When submitting a form with a dependent field if the dependent field is not shown then a "This form should not contain extra fields." error is shown.

$builder = new DynamicFormBuilder($builder);

$builder
    ->add('comparison', ChoiceType::class, [
        'choices' => [
            'is…' => 'eq',
            'is not…' => 'neq',
            'is empty' => 'empty',
        ]
    ])
    ->addDependent(
        name:'value',
        dependencies: ['comparison'],
        callback: function (DependentField $field, ?string $comparison) use ($options): void {
            if ($comparison === 'empty') {
                return;
            }
            $field->add(TextType::class, [
                'required' => false
            ]);
        }
    );

Video:

https://github.com/SymfonyCasts/dynamic-forms/assets/25124/fbec34dc-b3ec-4c64-ad35-81696c763d4d

I'm not using any javascript to remove the value field before submission so the data is being submitted triggering the error.

I've also implemented this with PRE_SUBMIT on the parent form which checks the view data, adds / removes the value field and unsets the data which works without error.

// Add an PRE_SET_DATA event listener that adds or removes the value field based on the original comparison data
// This fires when the data is set on the form
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($options) {
    $form = $event->getForm();
    $data = $event->getData();

    $comparison = $data['comparison'] ?? null;

    if (!in_array($comparison, [ComparisonType::EMPTY, ComparisonType::NOT_EMPTY])) {
        $form->add('value', TextType::class, ['required' => false]);
    }
});

// Add a PRE_SUBMIT event listener that adds or removes the value field based on the submitted comparison data
// This fires when the form is submitted
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
    $form = $event->getForm();
    $data = $event->getData();

    $comparison = $data['comparison'] ?? null;

    if (!in_array($comparison, [ComparisonType::EMPTY, ComparisonType::NOT_EMPTY])) {
        $form->add('value', TextType::class, ['required' => false]);
        return;
    }

    // Remove the form field
    $form->remove('value');
    // Unset the value and the submitted data
    // Removing the submitted data removes the extra fields error
    unset($data['value']);
    $event->setData($data);

});
bocharsky-bw commented 2 months ago

Yeah, if you don't use JS to drop the value field before form submit - then form events is the correct way to work around it. Thanks for sharing your solution with others!

If you have ideas on how to improve the package - please, feel free to open a PR

leevigraham commented 2 months ago

@bocharsky-bw experimenting with this some more and I think I found something that may be useful.

In my form I have added an additional "update" button that sets the following options:

image

If the 'update' button is clicked the form is submitted and all front end and back end validation is disabled.

Combine this with an updated on change listener which passes the update button through as the requester element:

image

and you have no errors on change:

https://github.com/SymfonyCasts/dynamic-forms/assets/25124/8f5cab70-c819-455e-974f-2980b02f4a09

Submitting the form with the submit button still triggers HTML5 and backend validation.

bocharsky-bw commented 2 months ago

Hey @leevigraham ,

Oh wow, this is an interesting workaround! Thanks for such a detailed description with the nice demo!

Hm, having 2 buttons is a bummer, but seems that do the trick, and probably the additional button might be hidden with CSS. Though I wonder if the behavior may be different in different browsers :/

So this workaround solves your issue? Do you think we can improve our docs by mentioning this?

I'm still not sure we're not missing something simple here... Btw, I found another possible solution - Symfony Forms have allow_extra_fields feature, see https://symfony.com/doc/current/reference/forms/types/form.html#allow-extra-fields . So using it in your form like:

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults([
        'allow_extra_fields' => true,
    ]);
}

Should also do the trick probably because will allow the form to accept additional fields not defined in the form type. However, it might pose security risks if not handled properly.

leevigraham commented 2 months ago

@bocharsky-bw allow_extra_fields definitely works… I'm looking for a solution that disables all validation and updates the form. During my experiments I've found that I can only disable validation for the current form and child forms (not bubble up validation to the root form). I'll keep exploring.

janklan commented 1 week ago

I think allowing extra fields is risky. Adding $field->add(HiddenType::class, ['mapped' => false]); instead seems to do the trick: it will register the field in the form, and because it's not mapped, it won't get into the resulting form data.