phpjqueryformssymfony

Form prototypes within prototypes - double $$name field


I need some help with prototypes that are within prototypes. Symfony is very clever with generating form prototypes, but once you are one layer down (i.e. prototypes within prototypes), it reuses $$name$$ for both prototypes.

This is what a prototype field looks like for my entity. JQuery swaps out $$name$$ with the correct index value (based on number of child nodes)

 <input type="text" id="entry_entities_$$name$$_contactFax" name="entry[entities][$$name$$][contactFax]" value="" />

So far so good. But when you go one level deeper, Symfony uses $$name$$ for the next level down too - here is a prototype for the entity property:

<div id="entry_entities_123_properties" data-prototype="    
    &lt;label for=&quot;entry_entities_$$name$$_properties_$$name$$_name&quot;&gt;Name&lt;/label&gt;
    &lt;input type=&quot;text&quot; id=&quot;entry_entities_$$name$$_properties_$$name$$_name&quot; name=&quot;entry[entities][$$name$$][properties][$$name$$][name]&quot; value=&quot;&quot; /&gt;

This means that (in this example with entity id 123) that all properties get ID 123:

name="entry[entities][123][properties][123][name]"
name="entry[entities][123][properties][123][name]"
name="entry[entities][123][properties][123][name]"

etc.

In my opinion the best way to solve the issue would be to use $$somethingelse$$ for the property - does anyone know where this is set - or does anyone have a complete example with JS on how to solve this? I embarked on a horrible find/replace of the second $$name$$ on each line, but it got very messy. I'm sure there is an easy way to do this, but I couldn't find any guides on the internet.


Solution

  • This code is for symfony 2.0 (in 2.1+ you can just pass the name to prototype() function):

    You can create your own collection type with your required option:

    <?php
    
    namespace YourBundle\Form\Type;
    
    use Symfony\Component\Form\AbstractType;
    use Symfony\Component\Form\FormBuilder;
    use Symfony\Component\Form\FormView;
    use Symfony\Component\Form\FormInterface;
    use Symfony\Component\Form\Extension\Core\EventListener\ResizeFormListener;
    use Symfony\Component\Form\Extension\Core\Type\CollectionType as BaseCollectionType;
    
    class CollectionType extends BaseCollectionType
    {
        /**
         * {@inheritdoc}
         */
        public function buildForm(FormBuilder $builder, array $options)
        {
            if ($options['allow_add'] && $options['prototype']) {
                $prototype = $builder->create($options['prototype_name'], $options['type'], $options['options']);
                $builder->setAttribute('prototype', $prototype->getForm());
            }
    
            $listener = new ResizeFormListener(
                $builder->getFormFactory(),
                $options['type'],
                $options['options'],
                $options['allow_add'],
                $options['allow_delete']
            );
    
            $builder
                ->addEventSubscriber($listener)
                ->setAttribute('allow_add', $options['allow_add'])
                ->setAttribute('allow_delete', $options['allow_delete'])
            ;
        }
    
        /**
         * {@inheritdoc}
         */
        public function getDefaultOptions(array $options)
        {
            $defaultOptions = parent::getDefaultOptions($options);
            $defaultOptions['prototype_name'] =  '$$name$$';
            return $defaultOptions;
        }
    }
    

    Then just define a service with:

    tags:
        - { name: form.type, alias: collection }
    

    And use it as Symfony's collection but with prototype_name parameter.