-->

Symfony 2 - rearrange form fields

2020-07-10 00:38发布

问题:

In our Symfony2 project we have a very complex structure for forms with embedded forms.... Now we got the requirement to bring the output of the form in an specific order.

And here is the problem: We use the form_widget(form) and now we are looking for a solution in the object (e.g. via annotations) or the formbuilder to move a specific field to the end of the form. in symfony 1.4 it was the widget-movefield() function, i guess...

Thx...

回答1:

You can re-order the fields using this bundle: https://github.com/egeloen/IvoryOrderedFormBundle

This allows you to do things like this:

$builder
->add('g', 'text', array('position' => 'last'))
->add('a', 'text', array('position' => 'first'))
->add('c', 'text')
->add('f', 'text')
->add('e', 'text', array('position' => array('before' => 'f')))
->add('d', 'text', array('position' => array('after' => 'c')))
->add('b', 'text', array('position' => 'first'));

This was going to be in core, but was rejected and pulled out into a bundle.



回答2:

Had same issue today with the form elements ordering.

Ended up with a trait that will override finishView method and reorder items in children property of a FormView:

trait OrderedTrait
{
    abstract function getFieldsOrder();

    public function finishView(FormView $view, FormInterface $form, array $options)
    {
        /** @var FormView[] $fields */
        $fields = [];
        foreach ($this->getFieldsOrder() as $field) {
            if ($view->offsetExists($field)) {
                $fields[$field] = $view->offsetGet($field);
                $view->offsetUnset($field);
            }
        }

        $view->children = $fields + $view->children;

        parent::finishView($view, $form, $options);
    }
}

Then in type implement getFieldsOrder method:

use OrderedTrait;

function getFieldsOrder()
{
    return [
        'first',
        'second',
        'next',
        'etc.',
    ];
}


回答3:

There's no need to "reorder" fields. All you need to do is to call form_label and/or form_widget for each field individually. Assuming you use Twig you could, for example, do:

<div>{{ form_label(form.firstName) }}</div>
<div>{{ form_widget(form.firstName) }}</div>

<div>{{ form_label(form.lastName) }}</div>
<div>{{ form_widget(form.lastName) }}</div>


回答4:

here is the solution I came up with.

I created a class that my types extend.

namespace WF\CORE\CoreBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class WfBaseForm extends AbstractType
{

    protected function useFields(FormBuilderInterface $builder, $fields)
    {
        foreach ($builder->all() as $field) {

            if (!in_array($field->getName(), $fields))
                $builder->remove($field->getName());
        }
    }


    public function reorder(FormBuilderInterface $builder, $keys = array())
    {
        $fields         = $builder->all();
        $ordered_fields = array_merge(array_flip($keys), $fields);
        $this->useFields($builder, array());

        foreach($ordered_fields as $field)
            $builder->add($field);

    }

    public function getName()
    {
        return 'base';
    }
}

Then you can use it in your inherited classes.

class AddressType extends WfBaseForm
{ 
public function buildForm(FormBuilderInterface $builder, array $options)
    {
        // I add as many fields as I need
        $builder->add( '…');
}

This class extends the one above

class ModifyAddressType extends BaseType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        parent::buildForm($builder, $options);

        $builder->add('title', 'text', array('constraints' => array(new NotBlank())));

        $this->reorder($builder, array('title', 'firstname', 'lastname'));
    }
}


回答5:

I had the same problem, but solved it in a different way. Here is my solution, as an idea for others who are searching for this problem.

You could add all the form fields in an event listener, because here you have all the objects data to decide on the fields order. You could for example use a method from the data object to decide on the fields order:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    // Don't use $builder->add(), or just for those fields which are always 
    // at the beginning of the form.

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

        // Now add all the fields in the order you need them to be, e.g by reading
        // the needed order from the data entry. 
        if ($entry->isFieldAFirst()) { 
             $form->add(fieldA);
             $form->add(fieldB);
        } else {
            $form->add(fieldB);
            $form->add(fieldA);
        }
    }
}


回答6:

As I understand, you want to use only form_widget(form) in final template.

Let assume we have two inherited models (ModelA, ModelB) and form types for them (ModelAType, ModelBType)

class ModelA {
  private $A;
  private $B;

  // Getters and setters
}

class ModelB extends ModelA {
  private $C;

  // Getters and setters
}

/**
 * @DI\Service(id = "form.type.modelA")
 * @DI\Tag("form.type", attributes={ "alias":"model_a_type" })
 */
class FormAType extends AbstractType {
  public function buildForm(FormBuilderInterface $builder, array $options)
  {
    $builder
      ->add('A')
      ->add('B')
    ;
  }

   // getName and so on
}

/**
 * @DI\Service(id = "form.type.modelA")
 * @DI\Tag("form.type", attributes={ "alias":"model_b_type" })
 */
class FormAType extends AbstractType {
  public function getParent() {
    return "model_a_type";
  }

  public function buildForm(FormBuilderInterface $builder, array $options)
  {
    $builder
      ->add('C')
    ;
  }

   // getName and so on
}

If you render formB, you will get A,B,C order, but you want A,C,B. To accomplish that, create form template and add reference to app. config file:

#app/config/config.yml
twig:
   ....
    form:
        resources:
            - YourAppBundle:Form:fields.html.twig


{# src/YourAppBundle/Resources/views/Form/fields.html.twig #}
{% block model_a_type_widget %}
   {{ form_widget(form.A) }}
   {{ form_widget(form.B) }}
{% endblock model_a_type_widget %}

{% block model_b_type_widget %}
   {{ form_widget(form.A) }}
   {{ form_widget(form.C) }}

   {{ block('model_a_type_widget') }}
{% endblock model_b_type_widget %}

Now, when you render formB, you will see desired order and keep code structured. It happens because every widget rendered only once, so if you render it before calling parent block, you will change their order.



回答7:

You can do it right in your controller before you render the template:

$form = ...

$view = $form->createView();

// Perform standard array operations to reorder: `$view->children`
// Here's a very simple example:

$firstField = $view->children['firstField'];
unset($view->children['firstField']);
$view->children['firstField'] = $firstField;

return array('form' => $view);

uasort works as well, however usort creates a non-associative array which will break the form rendering later on.



回答8:

{{ form_start(form) }}

    <div>{{ form_label(form.title) }}</div>
    <div>{{ form_widget(form.title,{'id': 'blog_title'} )}}</div>

    <div>{{ form_label(form.tag) }}</div>
    <div>{{ form_widget(form.tag,{'id': 'blog_tag'} )}}</div>

    <div>{{ form_label(form.category) }}</div>
    <div>{{ form_widget(form.category,{'id': 'blog_category'} )}}</div>

    <div>{{ form_label(form.image) }}</div>
    <div>{{ form_widget(form.image,{'id': 'blog_image'} )}}</div>

    <div>{{ form_label(form.body) }}</div>
    <div>{{ form_widget(form.body,{'id': 'editor'} )}}</div>

    <input type="submit" class="btn btn-success" value="Create" />
{{ form_end(form) }}