1
votes

I'm working on a basic CRUD application with Symfony-2.1 and Propel. Ideally, my model would be defined in the propel XML schema and the GUI would be automatically updated to reflect the changes. I don't think that will be possible within my given time restrictions, but I'm trying to get as close as possible.

My first compromise was to create the form types by hand, since the propel form types generator is rather rudimentary. But to stay at that level of manual adoption (e.g. upon adding a new model type) I'd like to solve the following problem entirely by definition of custom form types:

I have a moderately complex Title model, which has a one-to-many relationship to the TitleFragment model (representing the main, sub- and short titles of a historical publication). In my HTML form to edit a Title, I implemented the standard "Add one" behavior to allow for adding another TitleFragment (as described i.e. in the symfony cookbook).

The cookbook solution proposes a list structure for the form elements and I added two JavaScript indicator classes to trigger my jQuery control element generation code on that form element:

<form [...]>
 <ol class="collection-editor collection-sortable" 
      data-prototype="{{ form_widget(form.titleFragments.vars.prototype)|e }}">
    {% for titleFragment in form.titleFragments %}
        <li>
        {{ form_widget(titleFragment) }}
        </li>
    {% endfor %}
  </ol>
  [...]
</form>

I'd like to generate that markup automatically by using symfony form theming techniques for a custom form type. To achieve this, I inherited the symfony collection form type and just changed its name to make my custom form theme apply to that form:

class SortableCollectionType extends \Symfony\Component\Form\Extension\Core\Type\CollectionType {
    public function getName() {
        return 'sortableCollection'; 
    }
}

Now, I have to augment the default symfony form theme (form_div_layout.html.twig) by a sortableCollection_widget block to force rendering of my collection as a list and attach some JavaScript indicator classes to the list (like in code sample #1).

The problem is that in the template, the form.titleFragments depends on the model (and will always depend on a model because the data needs to be bound to the underlying collection of TitleFragments of the Title model).

class TitleType extends BaseAbstractType {

public function buildForm(FormBuilderInterface $builder, array $options) {

    $builder->add('titleFragments', // no choice for a generic name here

      new Form\DerivedType\SortableCollectionType(), array( 
        'type' => new TitlefragmentType(),
        'allow_add' => true,
        'by_reference' => false,
    ));
}
}

I thought about passing the name of the property (e.g. 'titleFragments') to the template, since twig supports dynamic access to object properties (using the attribute function) since version 1.2.

Has someone an idea how to get the data all the way from my custom form type to the twig template? That would be o.k. although it would be of course redundant and a little clumsy.

    $builder->add('titleFragments', 
      new Form\DerivedType\SortableCollectionType(), array( 
        'type' => new TitlefragmentType(),
        'options' => array('collectionPropertyName' => 'titleFragments'),

I didn't find the Symfony API for the form builder specifically helpful.

1

1 Answers

0
votes

To pass data to the view, the finishView method of a form type can be used.

public function finishView(FormView $view, FormInterface $form, array $options){
        $view->vars['modelClass'] = $this->modelClass;
}

Since the data_class is defined on the form types, I can even compute this value automatically. I also override the prototype element id placeholder (prototypeName) automatically if the collection contains nested types.

public function buildForm(FormBuilderInterface $builder, array $options)
{
    parent::buildForm($builder, $options);

    // extract unqualified class name and pass it to the view
    // this allows more accurate control elements (instead of "add component",
    // one can use "add modelClass")
    $dataClassStr = $options['type']->getOption('data_class');
    $parts = explode('\\', $dataClassStr);
    $this->modelClass = array_pop($parts);

    $prototypeName = '__' . $this->modelClass . 'ID__';

    if ($options['allow_add'] && $options['prototype']) {
        $prototype = $builder->create($prototypeName, $options['type'], array_replace(array(
            'label' => $options['prototype_name'] . 'label__',
        ), $options['options']));
        $builder->setAttribute('prototype', $prototype->getForm());
    }
}

In the twig template, the data passed to the view is available via the form.vars variable.

{% block sortableCollection_widget %}
{% spaceless %}

    {# [...] code copied from the default collection widget #}

    <a href="#" class="sortableCollectionWidget add-entity">
        {{ form.vars.modelClass|trans }} add one
    </a>

    {# For the javascript to have access to the translated modelClass name.
       The up and down sortable controls need this to be more expressive.   #}

    <input type="hidden" name="modelClassName" value="{{ form.vars.modelClass }}"/>

{% endspaceless %}
{% endblock %}