Form Events: A readonly Embedded Field
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeReady for the last challenge? All four of these genus scientists are already saved to the database. And while I guess it's kind of cool that I can change this scientist from one User
to another, it's also a little bit weird: When would I ever change a specific scientist from one User
to another? If this User
weren't studying this Genus
anymore, I should delete them. And if a new User
were studying this Genus
, we should probably just add a new GenusScientist
.
So I want to update the interface: when I hit "Add Another Scientist", I do want the User
select, just like now. But for existing genus scientists - the ones that are already saved to the database - I want to simply print the user's email in place of the drop-down.
In Symfony language, this means that I want to remove the user
field from the embedded form if the GenusScientist
behind it is already saved.
About Form Events
To do that, open the GenusScientistEmbeddedForm
. Guess what? We get to try a feature that I don't get to use very often: Symfony Form Events.
Here's the idea: every form has a life cycle: the form is created, initial data is set onto the form and then the form is submitted. And we can hook into this process!
Form Event Setup!
To do it, write addEventListener()
and then pass a constant FormEvents::POST_SET_DATA
. After that, say array($this, 'onPostSetData')
:
// ... lines 1 - 11 | |
use Symfony\Component\Form\FormEvents; | |
// ... lines 13 - 14 | |
class GenusScientistEmbeddedForm extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
// ... lines 20 - 27 | |
->addEventListener( | |
FormEvents::POST_SET_DATA, | |
array($this, 'onPostSetData') | |
) | |
; | |
} | |
// ... lines 34 - 50 | |
} |
Let's break that down: the POST_SET_DATA
is a constant for an event called form.post_set_data
. This is called after the data behind the form is added to it: in other words, after the GenusScientist
is bound to each embedded form.
When that happens, the form system will call an onPostSetData()
function, which we are about to create: public function onPostSetData()
. This will receive a FormEvent
object:
// ... lines 1 - 10 | |
use Symfony\Component\Form\FormEvent; | |
// ... lines 12 - 14 | |
class GenusScientistEmbeddedForm extends AbstractType | |
{ | |
// ... lines 17 - 34 | |
public function onPostSetData(FormEvent $event) | |
{ | |
// ... lines 37 - 40 | |
} | |
// ... lines 42 - 50 | |
} |
Now we're close! Inside, add an if statement: if $event->getData()
:This form is always bound to a GenusScientist
object. So this will return the GenusScientist
object bound to this form, or - if this is a new form - then it may return null
. That's why we'll say if $event->getData() && $event->getData()->getId()
:
// ... lines 1 - 14 | |
class GenusScientistEmbeddedForm extends AbstractType | |
{ | |
// ... lines 17 - 34 | |
public function onPostSetData(FormEvent $event) | |
{ | |
if ($event->getData() && $event->getData()->getId()) { | |
// ... lines 38 - 39 | |
} | |
} | |
// ... lines 42 - 50 | |
} |
In human-speak: as long as there is a GenusScientist
bound to this form and it's been saved to the database - i.e. it has an id value - then let's unset the user
field from the form.
To do that, fetch the form with $form = $event->getForm()
. Then, literally, unset($form['user'])
:
// ... lines 1 - 14 | |
class GenusScientistEmbeddedForm extends AbstractType | |
{ | |
// ... lines 17 - 34 | |
public function onPostSetData(FormEvent $event) | |
{ | |
if ($event->getData() && $event->getData()->getId()) { | |
$form = $event->getForm(); | |
unset($form['user']); | |
} | |
} | |
// ... lines 42 - 50 | |
} |
This $form
variable is a Form
object, but you can treat it like an array, including unsetting fields.
That's it for the form! The last step is to conditionally render the user
field. Because if we refresh right now, the form system yells at us:
There's no
user
field inside of our template at line 9.
Wrap that in an if statement: if genusScientistForm.user is defined
, then print it:
// ... lines 1 - 2 | |
{% macro printGenusScientistRow(genusScientistForm) %} | |
<div class="col-xs-4 js-genus-scientist-item"> | |
// ... lines 5 - 8 | |
{% if genusScientistForm.user is defined %} | |
{{ form_row(genusScientistForm.user) }} | |
// ... lines 11 - 12 | |
{% endif %} | |
// ... line 14 | |
</div> | |
{% endmacro %} | |
// ... lines 17 - 58 |
Else, use a strong tag and print the user's e-mail address with genusScientistForm.vars
- which is something we mastered in our Form Theming tutorial - .data
- which will be a GenusScientist
object - .user.email
:
// ... lines 1 - 2 | |
{% macro printGenusScientistRow(genusScientistForm) %} | |
<div class="col-xs-4 js-genus-scientist-item"> | |
// ... lines 5 - 8 | |
{% if genusScientistForm.user is defined %} | |
{{ form_row(genusScientistForm.user) }} | |
{% else %} | |
<strong>{{ genusScientistForm.vars.data.user.email }}</strong> | |
{% endif %} | |
// ... line 14 | |
</div> | |
{% endmacro %} | |
// ... lines 17 - 58 |
This says: find the GenusScientist
object behind this form, call getUser()
on it, and then call getEmail()
on that.
I think it's time to celebrate! Refresh the form. It looks exactly like I wanted. It's like my birthday! And when we add a new one, it still has the drop-down. You guys are the best!
Hi. I have a question. I tried to apply this readonly tips on my own app, but I'm facing a pb.
Example:
- I have 2 scientists for genus1, I add a new scientist. Then when I hit save, it deletes the 2 scientists and then add the new one.
- If I hit save without doing any changes, it deletes the actual genus scientists.
This is done as soon as I set up this readonly change
Any Idea?