Buy

Now that we've added the yearsStudied field to each GenusScientist, I'm not too sure that checkboxes make sense anymore. I mean, if I want to show that a User studies a Genus, I need to select a User, but I also need to tell the system how many years they have studied. How should this form look now?

Here's an idea, and one that works really well the form system: embed a collection of GenusScientist subforms at the bottom, one for each user that studies this Genus. Each subform will have a User drop-down and a "Years Studied" text box. We'll even add the ability to add or delete subforms via JavaScript, so that we can add or delete GenusScientist rows.

Creating the Embedded Sub-Form

Step one: we need to build a form class that represents just that little embedded GenusScientist form. Inside your Form directory, I'll press Command+N - but you can also right-click and go to "New" - and select "Form". Call it GenusScientistEmbeddedForm. Bah, remove that getName() method - that's not needed in modern versions of Symfony:

37 lines src/AppBundle/Form/GenusScientistEmbeddedForm.php
... lines 1 - 2
namespace AppBundle\Form;
... lines 4 - 8
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class GenusScientistEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
... lines 17 - 26
}
public function configureOptions(OptionsResolver $resolver)
{
... lines 31 - 33
}
}

Yay!

In configureOptions(), add $resolver->setDefaults() with the classic data_class set to GenusScientist::class:

37 lines src/AppBundle/Form/GenusScientistEmbeddedForm.php
... lines 1 - 4
use AppBundle\Entity\GenusScientist;
... lines 6 - 12
class GenusScientistEmbeddedForm extends AbstractType
{
... lines 15 - 28
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => GenusScientist::class
]);
}
... lines 35 - 36
}

We will ultimately embed this form into our main genus form... but at this point... you can't tell: this form looks exactly like any other. And it will ultimately give us a GenusScientist object.

For the fields, we need two: user and yearsStudied:

37 lines src/AppBundle/Form/GenusScientistEmbeddedForm.php
... lines 1 - 12
class GenusScientistEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user', EntityType::class, [
... lines 19 - 23
])
->add('yearsStudied')
;
}
... lines 28 - 36
}

We do not need a genus dropdown field: instead, we'll automatically set that property to whatever Genus we're editing right now.

The user field should be an EntityType dropdown. In fact, let's go to GenusFormType and steal the options from the genusScientists field - it'll be almost identical. Set this to EntityType::class and then paste the options:

37 lines src/AppBundle/Form/GenusScientistEmbeddedForm.php
... lines 1 - 5
use AppBundle\Entity\User;
use AppBundle\Repository\UserRepository;
... lines 8 - 12
class GenusScientistEmbeddedForm extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('user', EntityType::class, [
'class' => User::class,
'choice_label' => 'email',
'query_builder' => function(UserRepository $repo) {
return $repo->createIsScientistQueryBuilder();
}
])
... line 25
;
}
... lines 28 - 36
}

And make sure you re-type the last r in User and auto-complete it to get the use statement on top. Do the same for UserRepository. The only thing that's different is that this will be a drop-down for just one User, so remove the multiple and expanded options.

Embedding Using CollectionType

This form is now perfect. Time to embed! Remember, our goal is still to modify the genusScientists property on Genus, so our form field will still be called genusScientists. But clear out all of the options and set the type to CollectionType::class. Set its entry_type option to GenusScientistEmbeddedForm::class:

60 lines src/AppBundle/Form/GenusFormType.php
... lines 1 - 11
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
... lines 13 - 18
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 24 - 46
->add('genusScientists', CollectionType::class, [
'entry_type' => GenusScientistEmbeddedForm::class
])
;
}
... lines 52 - 58
}

Before we talk about this, let's see what it looks like! Refresh!

Woh! This Genus is related to four GenusScientists... which you can see because it built an embedded form for each one! Awesome! Well, it's mostly ugly right now, but it works, and it's free!

Try updating one, like 26 to 27 and hit Save. It even saves!

Rendering the Collection... Better

But let's clean this up - because the form looks awful... even by my standards.

Open the template: app/Resources/views/admin/genus/_form.html.twig:

26 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 21
{{ form_row(genusForm.genusScientists) }}
... lines 23 - 24
{{ form_end(genusForm) }}

This genusScientists field is not and actual field anymore: it's an array of fields. In fact, each of those field is itself composed of more sub-fields. What we have is a fairly complex form tree, which is something we talked about in our Form Theming Tutorial.

To render this in a more controlled way, delete the form_row. Then, add an h3 called "Scientists", a Bootstrap row, and then loop over the fields with for genusScientistForm in genusForm.genusScientists:

34 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 22
<h3>Scientists</h3>
<div class="row">
{% for genusScientistForm in genusForm.genusScientists %}
... lines 26 - 28
{% endfor %}
</div>
... lines 31 - 32
{{ form_end(genusForm) }}

Yep, we're looping over each of those four embedded forms.

Add a column, and then call form_row(genusScientistForm) to print both the user and yearsStudied fields at once:

34 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 22
<h3>Scientists</h3>
<div class="row">
{% for genusScientistForm in genusForm.genusScientists %}
<div class="col-xs-4">
{{ form_row(genusScientistForm) }}
</div>
{% endfor %}
</div>
... lines 31 - 32
{{ form_end(genusForm) }}

So this should render the same thing as before, but with a bit more styling. Refresh! Ok, it's better... but what's up with those zero, one, two, three labels?

This genusScientistForm is actually an entire form full of several fields. So, it prints out a label for the entire form... which is zero, one, two, three, and four. That's not helpful!

Instead, print each field by hand. Start with form_errors(genusScientistForm), just in case there are any validation errors that are attached at this form level:

36 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 22
<h3>Scientists</h3>
<div class="row">
{% for genusScientistForm in genusForm.genusScientists %}
<div class="col-xs-4">
{{ form_errors(genusScientistForm) }}
... lines 28 - 29
</div>
{% endfor %}
</div>
... lines 33 - 34
{{ form_end(genusForm) }}

It's not common, but possible. Then, simply print form_row(genusScientistForm.user) and form_row(genusScientistForm.yearsStudied):

36 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 22
<h3>Scientists</h3>
<div class="row">
{% for genusScientistForm in genusForm.genusScientists %}
<div class="col-xs-4">
{{ form_errors(genusScientistForm) }}
{{ form_row(genusScientistForm.user) }}
{{ form_row(genusScientistForm.yearsStudied) }}
</div>
{% endfor %}
</div>
... lines 33 - 34
{{ form_end(genusForm) }}

Try it! Much better!

But you know what we can't do yet? We can't actually remove - or add - new scientists. all we can do is edit the existing ones. That's silly! So let's fix it!

Leave a comment!

  • 2017-01-29 weaverryan

    Yo ehymel!

    I think it should look something like this:


    {% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">
    {# you don't have individual fields to render, you just have the ONE field to render #}
    {{ form_errors(genusScientistForm) }}
    {{ form_label(genusScientistForm, 'Genus Scientist') }}
    {{ form_widget(genusScientistForm) }}
    </div>
    {% endfor %}

    In the tutorial, genusScientistForm is really an embedded form, with sub-parts as you mentioned. But now, genusScientistForm is literally just a single field (EntityType)... without any sub-fields. So, you just need to render it like any other single field :). One simpler option might be something like this:


    {% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">
    {{ form_row(genusScientistForm, {
    label: 'Genus Scientist')
    } }}
    </div>
    {% endfor %}

    Not tested - but give it a try :).

    Cheers!

  • 2017-01-29 ehymel

    I really really appreciate you helping work this out!!

    It looks like the only problem with your proposed solution is that I don't seem to have access to the sub-parts of the added EntityType when rendering the form. Specifically if I try to get rid of the automatically generated labels ("0", "1", "2", etc) in the way you did it in the lesson, it fails, I assume because twig doesn't now how to get to it:


    {% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">
    {{ form_errors(genusScientistForm) }}
    {{ form_row(genusScientistForm.user) }} <------------------- this line fails
    </div>
    {% endfor %}

    Thoughts?

  • 2017-01-29 ehymel

    Genius! That works perfectly. Thanks very much.

    Glad to hear you had to think about it for a while :)

  • 2017-01-25 weaverryan

    Hi ehymel!

    Sorry for my slow reply - I needed to wait until I have a few minutes to really think about this :). So, the goal would be to have a collection of embedded forms (with the ability to add a new one, remove existing ones, etc), but each embedded form will have only the single, Scientist drop down. Is that correct?

    So, here's how to do it :). You will STILL have a CollectionType just like before. The only difference is that the "entry_type" (i.e. the form that's used in the collection) *won't* be an entirely different form class (e.g. GenusScientistEmbeddedForm), it will simply be the EntityType field. Something like this:


    $builder
    ->add('genusScientists', CollectionType::class, [
    'entry_type' => EntityType::class,
    'entry_options' => [
    'class' => User::class,
    'choice_label' => 'email'
    ],
    'allow_delete' => true,
    'allow_add' => true,
    ])

    ... and that's it! In your template, rendering is simpler, something more like this:


    {% for genusScientistForm in genusForm.genusScientists %}
    <div class="col-xs-4 js-genus-scientist-item">



    {# you don't have individual fields to render, you just have the ONE field to render #}
    {{ form_row(genusScientistForm) }}
    </div>
    {% endfor %}

    If you were doing this from the *inverse* side instead, then you would just need to make sure to follow the same steps we did here: https://knpuniversity.com/s...

    Let me know if that helps! And cheers!

  • 2017-01-22 ehymel

    In your lesson for using CollectionType, you have changed from ManyToMany to matching OneToMany and ManyToMany with a new entity to manage the relationship. All makes sense. Your embedded form for multiple drop-down boxes uses that new entity to persist changes to the relationship in the db.

    I would like to leave things as a ManyToMany relationship (like in your earlier lessons) but use drop-down boxes like you use in this lesson. However, it doesn't seem to work. I'm listing the entity of the inverse side of the relationship in the embedded form (so I can list choices in the drop-down select field, but when I save it modifies only this entity (inverse side of ManyToMany) rather than the relationship.

    In other words, using examples from your entities: Owning entity; Genus. Inverse side: Scientist. How to embed multiple drop-down list of scientists that in the same way you do when there is a named entity joining the two, but instead with the ManyToMany relationship itself defining things? I've tried the equivalent of adding an embedded form of Scientists drop-down list and embedding that into the Genus edit page. I get multiple drop-down lists, but the correct items are not pre-selected, and changing/saving updates the Scientist list itself rather than the relationship.