If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
There's still one ugly problem with our form, and I promised we would fix: when we click "Add Another Scientist"... well, it don't look right!. The new form should have the exact same styling as the existing ones.
Why does it look different, anyways? Remember the data-prototype
attribute?
{{ form_start(genusForm) }} | |
... lines 2 - 23 | |
<div class="row js-genus-scientist-wrapper" | |
data-prototype="{{ form_widget(genusForm.genusScientists.vars.prototype)|e('html_attr') }}" | |
... line 26 | |
> | |
... lines 28 - 41 | |
</div> | |
... lines 43 - 44 | |
{{ form_end(genusForm) }} |
By calling form_widget
, this renders a blank GenusScientist
form... by using the
default Symfony styling. But when we render the existing embedded forms, we
wrap them in all kinds of cool markup:
{{ form_start(genusForm) }} | |
... lines 2 - 27 | |
{% for genusScientistForm in genusForm.genusScientists %} | |
<div class="col-xs-4 js-genus-scientist-item"> | |
<a href="#" class="js-remove-scientist pull-right"> | |
<span class="fa fa-close"></span> | |
</a> | |
{{ form_errors(genusScientistForm) }} | |
{{ form_row(genusScientistForm.user) }} | |
{{ form_row(genusScientistForm.yearsStudied) }} | |
</div> | |
{% endfor %} | |
... lines 38 - 44 | |
{{ form_end(genusForm) }} |
What we really want is to somehow make the data-prototype
attribute use the markup
that we wrote inside the for
statement.
How? Well, there are at least two ways of doing it, and I'm going to show you the less-official and - in my opinion - easier way!
Head to the top of the file and add a macro called printGenusScientistRow()
that
accepts a genusScientistForm
argument:
... lines 1 - 2 | |
{% macro printGenusScientistRow(genusScientistForm) %} | |
... lines 4 - 11 | |
{% endmacro %} | |
... lines 13 - 52 |
If you haven't seen a macro before in Twig, it's basically a function that you create right inside Twig. It's really handy when you have some markup that you don't want to repeat over and over again.
Next, scroll down to the scientists area and copy everything inside the for
statement.
Delete it, and then paste it up in the macro:
... lines 1 - 2 | |
{% macro printGenusScientistRow(genusScientistForm) %} | |
<div class="col-xs-4 js-genus-scientist-item"> | |
<a href="#" class="js-remove-scientist pull-right"> | |
<span class="fa fa-close"></span> | |
</a> | |
{{ form_errors(genusScientistForm) }} | |
{{ form_row(genusScientistForm.user) }} | |
{{ form_row(genusScientistForm.yearsStudied) }} | |
</div> | |
{% endmacro %} | |
... lines 13 - 52 |
To call that macro, you actually need to import it... even though it already
lives inside this template. Whatever: you can do that with {% import _self as formMacros %}
:
{% import _self as formMacros %} | |
... lines 2 - 52 |
The _self
part would normally be the name of a different template whose macros
you want to call, but _self
is a magic way of saying, no, this template.
The formMacros
is an alias I just invented, and it's how we will call the macro.
For example, inside the for
loop, render formMacros.printGenusScientistRow()
and
pass it genusScientistForm
:
... lines 1 - 13 | |
{{ form_start(genusForm) }} | |
... lines 15 - 40 | |
{% for genusScientistForm in genusForm.genusScientists %} | |
{{ formMacros.printGenusScientistRow(genusScientistForm) }} | |
{% endfor %} | |
... lines 44 - 50 | |
{{ form_end(genusForm) }} |
And now we can do the same thing on the data-prototype
attribute:
formMacros.printGenusScientistRow()
and pass that genusForm.genusScientists.vars.prototype
.
Continue to escape that that into HTML entities:
... lines 1 - 13 | |
{{ form_start(genusForm) }} | |
... lines 15 - 36 | |
<div class="row js-genus-scientist-wrapper" | |
data-prototype="{{ formMacros.printGenusScientistRow(genusForm.genusScientists.vars.prototype)|e('html_attr') }}" | |
... line 39 | |
> | |
... lines 41 - 47 | |
</div> | |
... lines 49 - 50 | |
{{ form_end(genusForm) }} |
I love when things are this simple! Go back, refresh, and click to add another scientist. Much, much better! Obviously, we need a little styling help here with our rows but you guys get the idea.
The last problem with our form deals with JavaScript. Go to /admin/genus
and
click "Add". Well... our fancy JavaScript doesn't work here. Wah wah.
But that makes sense: we put all the JavaScript into the edit template. The fix for
this is super old-fashioned... and yet perfect: we need to move all that JavaScript
into its own file. Since this isn't a JavaScript tutorial, let's keep things simple:
in web/js
, create a new file: GenusAdminForm.js
.
Ok, let's be a little fancy: add a self-executing block: a little function that calls itself and passes jQuery inside:
(function ($) { | |
... lines 2 - 32 | |
})(jQuery); |
Then, steal the code from edit.html.twig
and paste it here. It doesn't really matter,
but I'll use $
everywhere instead of jQuery
to be consistent:
(function ($) { | |
$(document).ready(function() { | |
var $wrapper = $('.js-genus-scientist-wrapper'); | |
$wrapper.on('click', '.js-remove-scientist', function(e) { | |
e.preventDefault(); | |
$(this).closest('.js-genus-scientist-item') | |
.fadeOut() | |
.remove(); | |
}); | |
$wrapper.on('click', '.js-genus-scientist-add', function(e) { | |
e.preventDefault(); | |
// Get the data-prototype explained earlier | |
var prototype = $wrapper.data('prototype'); | |
// get the new index | |
var index = $wrapper.data('index'); | |
// Replace '__name__' in the prototype's HTML to | |
// instead be a number based on how many items we have | |
var newForm = prototype.replace(/__name__/g, index); | |
// increase the index with one for the next item | |
$wrapper.data('index', index + 1); | |
// Display the form in the page before the "new" link | |
$(this).before(newForm); | |
}); | |
}); | |
})(jQuery); |
Back in the edit template, include a proper script tag: src=""
and pass in the
GenusAdminForm.js
path:
... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
<script src="{{ asset('js/GenusAdminForm.js') }}"></script> | |
{% endblock %} | |
... lines 8 - 20 |
Copy the entire javascripts
block and then go into new.html.twig
. Paste!
... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
<script src="{{ asset('js/GenusAdminForm.js') }}"></script> | |
{% endblock %} | |
... lines 8 - 20 |
And now, we should be happy: refresh the new form. Way better!
But... what's with that random label - "Genus scientists" - after the submit button! What the crazy!?
Ok, so the reason this is happening is a little subtle. Effectively, because there
are no genus scientists on this form, Symfony sort of thinks that this genusForm.genusScientists
field was never rendered. So, like all unrendered fields, it tries to render it in
form_end()
. And this causes an extra label to pop out.
It's silly, but easy to fix: after we print everything, add form_widget(genusForm.genusScientists)
.
And ya know what? Let's add a note above to explain this - otherwise it looks a little
crazy.
... lines 1 - 13 | |
{{ form_start(genusForm) }} | |
... lines 15 - 36 | |
<div class="row js-genus-scientist-wrapper" | |
... lines 38 - 39 | |
> | |
... lines 41 - 47 | |
</div> | |
{# prevents weird label from showing up in new #} | |
{{ form_widget(genusForm.genusScientists) }} | |
... lines 51 - 52 | |
{{ form_end(genusForm) }} |
And don't worry, this will never actually print anything. Since all of the children fields are rendered above, Symfony knows not to re-render those fields. This just prevents that weird label.
Refresh! Extra label gone. And if you go back and edit one of the genuses, things look cool here too.
Now, I have one last challenge for us with our embedded forms.
// composer.json
{
"require": {
"php": ">=5.5.9",
"symfony/symfony": "3.1.*", // v3.1.4
"doctrine/orm": "^2.5", // v2.7.2
"doctrine/doctrine-bundle": "^1.6", // 1.6.4
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
"symfony/swiftmailer-bundle": "^2.3", // v2.3.11
"symfony/monolog-bundle": "^2.8", // 2.11.1
"symfony/polyfill-apcu": "^1.0", // v1.2.0
"sensio/distribution-bundle": "^5.0", // v5.0.22
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
"incenteev/composer-parameter-handler": "^2.0", // v2.1.2
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
"doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
"stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.0.7
"symfony/phpunit-bridge": "^3.0", // v3.1.3
"nelmio/alice": "^2.1", // 2.1.4
"doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
}
}