Symfony2: Outputting form checkboxes in a hierarchy

Recently when I was working on a client project we had a bunch of permissions which had a hierarchy (or tree structure). For example, you needed Permission 1 to have Permission 1a and Permission 1b. In the examples below lets assume `$choices` is equal to the following:

At first, I used the built in in optgroups of a the select box to output the form, so it was clear what permissions fell where. My form would look similar to:

Multiple select boxes aren’t the easiest to work with as we all know. Also, it isn’t as easy to visually see the difference as the height of the select box could not be long enough to show you what an optgroup’s title is. Instead, I decided to use the checkbox approach. Issue with this, the current Symfony2 form themes don’t output checkboxes in groups or with any visual indication of the hierarchy. I ended up creating my own custom field type so I could customize the way it renders globally via the form themeing. My custom type just always set the choice options to expanded and multiple as true. For the actual rendering, below is what I ended up with.

The above is assuming you are using bootstrap to render your forms as it has those classes. My listless class just sets the ul list style to none. The code should be fairly easy to follow, basically it goes through and any sub-array (an optgroup) it will nest in the list from the previous option. This method does assume that you have the ‘parent’ node before the nested array. I also in the bottom have some javascript that basically makes sure that you can’t check off a sub-group if the parent is not checked. When you first check the parent, it selects all the children. For the example I just put the javascript in there, it uses and id attribute, so you can only have one of these per page. If you were using this globally, I’d recommend tagging the UL with a data attribute and moving the javascript into a global JS file.

Since a picture is worth a thousand words, here is an example of what it looks like working:

permissions-example

Let me know if you have any questions! Happy Friday.

Posted In: jQuery, PHP, Symfony, Tips n' Tricks

Tags: , , , , ,

  • Mikhail Kulebyakin

    Hello! Is it possible to change toplevel (Permission 1,2,3) to radio, but sub level (1a,1b) left as checkbox?

  • Hey –

    You’d need to output the “else” part as radio widgets rather than checkboxes. Then you’d need to use a custom validator on the backend to validate everything.

  • Daniel Oliveira

    You have a full example? How you pass data to create Form? notice: $this->formCreateBuilder(DATA?); ? thanks!

  • Depends on how you want to pass it. You could pass it via the options on the form or even the constructor, ie public function __construct($options){}… Then when you create it via $this->createForm(new MyFormType($options))…Although the options resolver route can give you a bit more in terms of requirements/optional etc.

  • Daniel Oliveira

    Yes, I use ->createForm(new FormType(), $dataStructure) but handleRequest() don’t work, and getData() return again $dataStructure.

  • I’m not sure what you mean? The getData() returns the data of the entire form. If your $dataStructure variable is what you are trying to pass as the choices you could do $this->createForm(new FormType($dataStructure)); that passes it in as an argument to the constructor.

  • Daniel Oliveira

    I solved! My problem was generated because I was passed as DATA parameter (array(a, b, c)) the request had (a, b) but returned the form (a, b, c). I Removed the second parameter of ->CreateForm(new FormType (), X) and FormType changed ‘mapped’ =>false to ‘mapped’=>true. Thanks man!

  • nulmail

    I have this error in twig:

    An exception has been thrown during the rendering of a template (“Notice: Undefined offset: 1”) in AcmeSettingsBundle:Settings:index.html.twig at line 34.

    Code:

    29: {% if children is iterable %}
    30:
    31: {% for child,choiceView in children %}
    32:
    33:
    34: {{ form_widget(form.offsetGet(child)) }}

  • Weird – what’s your choices widget look like in your form in terms of how you are initializing it.

  • nulmail

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
    $builder->add(‘permissions’, ‘checkbox_hierarchy’, [
    ‘choices’ => [
    ‘Permission 1’ => ‘Permission 1’,
    ‘Permission 1 Sub-Permissions’ => [
    ‘Permission 1a’ => ‘1a’,
    ‘Permission 1b’ => ‘1b’
    ],
    ‘Permission 2’ => ‘Permission 2’,
    ‘Permission 3’ => ‘Permission 3’,
    ‘Permission 3 Sub-Permissions’ => [
    ‘Permission 3a’ => ‘3a’,
    ‘Permission 3b’ => ‘3b’
    ]
    ],
    ‘multiple’ => true
    ]
    );
    }

  • nulmail

    /** @var $formFactory FOSUserBundleFormFactoryFactoryInterface */
    $formFactory = $this->container->get(‘fos_user.group.form.factory’);
    $form = $formFactory->createForm();

  • Try adding an expanded=>true

  • nulmail

    Thanks Matt, It works ! :) But why work only with “expanded” =>true ?

  • Without it as expanded it is rendered totally differently (as a multi-select) so the code won’t be able to pull each checkbox out.

  • pavan ganvani

    How can i pass entity path in this instead of array?

  • Do you mean how do you pass a list of entities? Not sure I follow what you are trying to do. You could try to do something like $choices = [ $entity->getId() => $entity, $entity2->getId() => $entity2 ]

  • Ahmed Bhs

    Can you show us your entity , i need some inforamtion about your relations

  • You can try to remove the JS in the example to make sure that they load checked. Otherwise my field is just a json_array, and what is in the example above is exactly the same code I use.

  • Ahmed Bhs

    Why when i summit the form like that (1) https://uploads.disquscdn.com/images/3913af407be12d97fa6d3cd2d132ab42ab5b58ad78665f5918b4dd680da53c4e.png

    It gives me this result (2) (all the childs are cheked) https://uploads.disquscdn.com/images/4cdfa6922de19967a4d67de7749d8855c7d5424656cdebe8398757818f3a685d.png ??
    I think it’s a js problèm, because the information is ok in the database
    Can you help me to solve the problèm plz

  • Ahmed Bhs

    ok thank it works even with js, it’s my prob tahnk you for replying