Skip to content

[Form] Add position support #11241

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
wants to merge 3 commits into from
Closed

Conversation

egeloen
Copy link

@egeloen egeloen commented Jun 26, 2014

Q A
Bug fix? no
New feature? yes
BC breaks? yes
Deprecations? no
Tests pass? yes
Fixed tickets #5827 #4254 #3452
License MIT
Doc PR TODO

This PR migrates egeloen/ivory-ordered-form into the core. It implements the feature the way @webmozart has explained in #5827. The UPGRADE file needs some love according to the BC promise but first, I would like some feedback :)


asort($this->weights, SORT_NUMERIC);

return array_keys($this->weights);
Copy link
Member

Choose a reason for hiding this comment

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

you should reset the properties at the end of the sorting (this will also help with garbage collection by removing the references inside this object)

Copy link
Author

Choose a reason for hiding this comment

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

I have removed the form reference stored in the orderer. For the reset at the end of the ordering, I don't think it is mandatory because all references have been cleaned at this state except if an error is detected during the ordering.

@egeloen egeloen added the Form label Jun 29, 2014
@egeloen
Copy link
Author

egeloen commented Jul 28, 2014

ping @webmozart

@egeloen
Copy link
Author

egeloen commented Nov 14, 2014

I have rebased this PR according to the last changes. ping @webmozart

@egeloen
Copy link
Author

egeloen commented Nov 14, 2014

Should I close it and reopen it against the 2.7 branch?

@webmozart
Copy link
Contributor

Sorry for the late reply. I like your work @egeloen! I'm 👍 for this except for a few things:

  • I think you misspelled "deferred" as "differred". That had me confused a bit.
  • Please remove FormOrdererInterface and the injection point of the orderer. I think the FormOrderer should be instantiated in ResolvedFormType directly. For me, this is an implementation detail that should not be exchangeable.
  • The doc blocks in FormOrderer don't help a lot. You don't need to write super long descriptions, but can you add a few sentences that explain what each method is doing? That's not entirely obvious.

With these changes and a rebase on 2.8, I'm happy to merge this. :)

@fabpot
Copy link
Member

fabpot commented Jan 25, 2016

@egeloen Are you still interested in finishing this one?

@egeloen
Copy link
Author

egeloen commented Jan 25, 2016

It was not clear to me what was the position of the symfony team about this feature. If we all agree, i can indeed finish it :)

@fabpot
Copy link
Member

fabpot commented Jan 25, 2016

@webmozart said he was ok, so I think we can go ahead and add this feature.

@egeloen egeloen force-pushed the form-orderer branch 3 times, most recently from 4b47fcd to 8b0d12a Compare January 27, 2016 15:24
@egeloen
Copy link
Author

egeloen commented Jan 27, 2016

@fabpot @webmozart I have rebased the PR as well as applied your comments. I'm not sure I can add a method to an interface according to the BC promise. Am I allowed? Additionally, the failling build seems unrelated. Anyway, let's review it.

@fabpot
Copy link
Member

fabpot commented Jan 27, 2016

Adding a method on an interface is not possible. If that makes sense (I haven't reviewed the PR), you can add a new interface.

*
* @return self The configuration object.
*/
public function setPosition($position);
Copy link
Member

Choose a reason for hiding this comment

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

Unfortunately, that's not possible as this is a BC break.

@egeloen
Copy link
Author

egeloen commented Jan 27, 2016

@fabpot Okay, so this PR still need some work as I'm adding methods to existing interfaces. @webmozart How would you name this new interface?

@fabpot
Copy link
Member

fabpot commented Jun 16, 2016

@egeloen Use a semi-random interface name for now so that we can move forward. Renaming it to something won't be that difficult.

@egeloen egeloen force-pushed the form-orderer branch 2 times, most recently from f49722b to 6a80046 Compare June 18, 2016 09:03
@egeloen
Copy link
Author

egeloen commented Jun 18, 2016

@fabpot Done

}

if (is_string($position) && ($position !== 'first') && ($position !== 'last')) {
throw new InvalidConfigurationException('If you use position as string, you can only use "first" & "last".');
Copy link
Member

Choose a reason for hiding this comment

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

first or last?

Copy link
Member

Choose a reason for hiding this comment

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

When using position as a string, the only supported values are "first" and "last".

/**
* Processes an array position (before/after).
*
* FIXME
Copy link
Member

Choose a reason for hiding this comment

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

still?

@liverbool
Copy link
Contributor

Is position option supported for Symfony\Component\Form\FormInterface::add?

@HeahDude
Copy link
Contributor

HeahDude commented Jul 4, 2016

Although I agree such a feature would be great, I don't think the implementation should be at the builder level. My guess is that should be done at the FomView level.

Having an order or another only impacts templates, and one could need different orders for different templates. Thus this should not lead to different building processes but different rendering.

@liverbool
Copy link
Contributor

liverbool commented Jul 4, 2016

I thinks it should done in all level builder, view, form.children especially form.children for sometime we want to submit some children before/after another children, most of times some field depended on others so ordering on form.children level will make it easy without re-build form.

@HeahDude
Copy link
Contributor

HeahDude commented Jul 5, 2016

@liverbool In which case do you need an order on submission?

If you have dependent fields, you should use listeners to rely either on submitted data (PRE_SUBMIT) or model data (POST_SUBMIT).
But the order of the fields of the submitted data array should not be responsible for changing the global behavior (e.g building) of the form.

@liverbool
Copy link
Contributor

liverbool commented Jul 5, 2016

@HeahDude First i'm not Symfony Guru ;)

Refer to symfony-cmf/media-bundle#9

Common case check for deletion

form:
    checkbox: remove_image
    image: image_field
    addEventListener(): check is remove_image checked

# We need to add addEventListener to every root form that contains image field.

My case

form:
    checkbox: remove_image
    custom_image: image_field
        addEventListener(): "can't check is remove_image checked"

# I need to add addEventListener to custom custom_image but i can't get submitted state of remove_image checkbox.

My solution

form:
    custom_image: image_field # compound field
        image: truly_image_field
        checkbox: remove_image
        addEventListener(): (check is remove_image checked)
        addModelTransformer(): (transform truly_image_field to model)

My discussion

custom_image: image_field
    addEventListener(): 
        - add remove_image to root form by making it's submition fire before custom_image field
        - check is remove_image checked when make sure remove_image was submitted first

form:
    custom_image: image_field

@HeahDude
Copy link
Contributor

HeahDude commented Jul 5, 2016

@liverbool Let's not mix submission and fields ordering.

I really think that a submission depending on the fields order is very fragile. The solution you wrote is the way to go: a custom type with a specific needs so with event listeners, data transformers and data mappers when useful.

@liverbool
Copy link
Contributor

liverbool commented Jul 6, 2016

@HeahDude I agree order submission quite weak but we can guarantee the ordering by using build position order by programmatic

@henrypenny
Copy link

henrypenny commented Aug 3, 2016

I'm a bit late to the party. Sorry.

One could argue that field order is presentation logic. Outside of their presentation, field order doesn't really matter to a form. HTTP doesn't care, relational databases don't generally care. Can't comment about nosql, perhaps field order is a concern there.

With that in mind, perhaps having field ordering in the form is not the way to go. A mechanism for sorting children within FormViews seems a little more abstracted.

$builder->add('field1');
$builder->add('field2');

...

$builder->add('field3');

...

$view->orderChildren(['field1', 'field3', 'field2']);

This will probably be very simple to implement as it's just an array manipulation.

It also allows one to easily specify the sorting in a template:

{{ form.orderChildren(['field1', 'field3', 'field2]) }}

Seems more MVC to keep presentation logic in the view, and it might not cause any BC breaks.

@henrypenny
Copy link

Adding this method to FormView works:


    public function orderChildren($fields)
    {
        $children = $this->children;
        $_children = array();

        foreach($fields as $field) {
            if(isset($children[$field])) {
                $_children[$field] = $children[$field];
                unset($children[$field]);
            }
            else {
                // throw an Exception ?
            }
        }

        foreach($children as $field => $child) {
            $_children[$field] = $child;
        }

        $this->children = $_children;
    }

There's probably a nicer way to write the code. But it works.

    public function finishView(FormView $view, FormInterface $form, array $options)
    {
            $view->orderChildren(['postCode', 'emailAddress']);
    }

Or in twig:

{{ form.orderChildren(['postCode', 'firstName', 'emailAddress']) }}

@nicolas-grekas nicolas-grekas added this to the 3.x milestone Dec 6, 2016
@backbone87
Copy link
Contributor

I agree with @henrypenny that this is a presentation logic issue and should not be implemented in terms of FormInterface. Moreover it could even hurt custom form processing logic (via listeners), when the order of subforms in the FormInterface is manipulated from arbitrary places (3rd party form type extensions). Based on this, i dont think any changes are necessary at all, if we compare an eventual FormView::orderChildren method with the status quo:

{% form_order my_form [ 'field1', 'field2', 'field3' ] %}
{{ form(my_form) }}

vs

{% form_order my_form [ 'field1', 'field2', 'field3' ] %}
{{ form_start(my_form) }}
  {{ form_widget(my_form) }}
{{ form_end(my_form) }}

vs

{{ form_start(my_form) }}
  {{ form_errors(my_form) }}
  {{ form_row(my_form.field1) }}
  {{ form_row(my_form.field2) }}
  {{ form_row(my_form.field3) }}
{{ form_end(my_form) }}

Only the first example is significantly smaller, than the others, while also being the simplest. As soon as you start dealing with compound subforms and want to interleave their output FormView::orderChildren wouldnt be useable anymore (or any other logic outside of the template).

@fabpot
Copy link
Member

fabpot commented Mar 22, 2017

Closing this old PR as it does not seem that there is a consensus. Having order only at the template level seems like the best way to go.

@fabpot fabpot closed this Mar 22, 2017
@nicolas-grekas nicolas-grekas modified the milestones: 3.x, 3.3 Mar 24, 2017
Nyholm added a commit that referenced this pull request Apr 14, 2021
This PR was squashed before being merged into the 5.3-dev branch.

Discussion
----------

[Form] Add support for sorting fields

| Q             | A
| ------------- | ---
| Branch?       | 5.x
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #5827
| License       | MIT
| Doc PR        | TODO

This PR adds a new `priority` option used later to sort all fields in order of importance instead of simply numerical order, i.e. fields with higher priority will be rendered first, fields with lower priority will be rendered last. The default priority would be "0" for all fields. Fields with equal priority will keep the original order (stable sorting).

History of previous proposals and discussions: #3452, #4254, #5827, #6111, #11241

This kind of feature has been abandoned in the past because complex proposals, and somehow rejected because ideally we should do the sorting at the view level ([customizing form](https://symfony.com/doc/current/form/form_customization.html) themes or layout templates) and it's true for most cases (the simple ones I think) but the fact is that it's often quite complex to accomplish that way, mainly for [dynamic forms](https://symfony.com/doc/current/form/dynamic_form_modification.html).

Let's focus the following analysis on explaining *why* and *when* the `priority` option could save us a lot of time, getting rid of cumbersome workarounds with templates to change the rendering order *only*.

---

A common example could be the building of a multi-steps form with a convenient type hierarchy and including fields dynamically based on the data. Let's take this sample:

![image](https://user-images.githubusercontent.com/2028198/113465635-a5a81180-9403-11eb-839f-3a32d5f84f47.png)

```php
class WorkflowType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // get current enabled transitions from the workflow definition...

        foreach ($enabledTransitions as $transitionName) {
            $builder->add($transitionName, SubmitType::class, ['attr' => ['value' => $transitionName]]);
        }
    }
}

class PersonRegistrationType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('firstName')
            ->add('lastName');

        $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
            $person = $event->getData();

            if ($person->isLegalType()) {
                $event->getForm()->add('company');
            }
        });
    }

    public function getParent()
    {
        return WorkflowType::class;
    }
}
```
These classes model the required form. However, according to the form building process and taken into account that the underlaying person data was set with "legal" type (from the previous step), this will be the rendering result:
```html
<button type="submit" name="form[register]" value="register">Register</button> {# wrong place #}
<input name="form[first_name]">
<input name="form[last_name]">
<input name="form[company]">  {# wrong place #}
```
Now, taking the view customization path to fix the order, you likely will face some problems regarding to:
 - How do I render the dynamic submit buttons at the bottom?
 - How can the specific form fields be rendered in the correct order if they vary from step to step? (being it a generic template)
 - if you solve the previous questions, you will also need to check if the company field is defined or not before rendering it in the right order.

There could be more and different problems depending on the context and you can find workarounds using the block prefixes system to customize each step block theme, but to me it's enough to think about other alternatives.

On the other side, using the `priority` option, the solution is quite easy. Setting the right priority value will guarantee the right rendering order:
```php
// ...
$builder->add($transitionName, SubmitType::class, ['priority' => -1, // ...]);

// ...
$event->getForm()->add('company', null, ['priority' => 1]);
```
That's ! There is no need to customize templates at all when you only need to change the fields order.

This way, these fields are being pre-defined in the order they will be rendered at view level. This sorting process will take place on `$form->createView()` method, just after `buildView()` and before `finishView()` calls.

Could we reconsider the usefulness of this feature? please.

Cheers!

Commits
-------

62650bb [Form] Add support for sorting fields
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.