CollectionType: Adding New with the Prototype
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 SubscribeSo, how can we add a new scientist to a Genus
?
Here's the plan: I want to add a button called "Add New Scientist", and when the user clicks it, it will render a new blank, embedded GenusScientist
form. After the user fills in those fields and saves, we will insert a new record into the genus_scientist
table.
The allow_add Option
Let's start with the front end first. Open GenusFormType
. After the allow_delete
option, put a new one: allow_add
set to true
:
// ... lines 1 - 18 | |
class GenusFormType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
// ... lines 24 - 46 | |
->add('genusScientists', CollectionType::class, [ | |
// ... lines 48 - 49 | |
'allow_add' => true, | |
'by_reference' => false, | |
]) | |
; | |
} | |
// ... lines 55 - 61 | |
} |
Remember: allow_delete
says:
It's ok if one of the genus scientists' fields are missing from the submitted data.
And when one is missing, the form should remove it from the genusScientists
array.
The allow_add
option does the opposite:
If there is suddenly an extra set of
GenusScientist
form data that's submitted, that's great!
In this case, it will create a new GenusScientist
object and set it on the genusScientists
array.
JavaScript Setup!
So, cool! Now open the _form.html.twig
template. Add a link and give it a class: js-genus-scientist-add
. Inside, give it a little icon - fa-plus-circle
and say "Add Another Scientist":
{{ form_start(genusForm) }} | |
// ... lines 2 - 23 | |
<div class="row js-genus-scientist-wrapper" | |
// ... lines 25 - 26 | |
> | |
// ... lines 28 - 37 | |
<a href="#" class="js-genus-scientist-add"> | |
<span class="fa fa-plus-circle"></span> | |
Add Another Scientist | |
</a> | |
</div> | |
// ... lines 43 - 44 | |
{{ form_end(genusForm) }} |
Love it! Time to hook up the JavaScript: open edit.html.twig
. Attach another listener to $wrapper
: on click
of the .js-genus-scientist-add
link. Add the amazing e.preventDefault()
:
// ... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
<script> | |
jQuery(document).ready(function() { | |
var $wrapper = $('.js-genus-scientist-wrapper'); | |
// ... lines 9 - 17 | |
$wrapper.on('click', '.js-genus-scientist-add', function(e) { | |
e.preventDefault(); | |
// ... lines 20 - 35 | |
}); | |
}); | |
</script> | |
{% endblock %} | |
// ... lines 40 - 52 |
So... what exactly are we going to do in here? We somehow need to clone one of the embedded GenusScientist
forms and insert a new, blank version onto the page.
Using... the prototype!
No worries! Symfony's CollectionType
has a crazy thing to help us: the prototype
.
Google for "Symfony form collection" and open the How to Embed a Collection of Forms document on Symfony.com. This page has some code that's ripe for stealing!
First, under the "Allowing New" section, find the template and copy the data-prototype
attribute code. Open our form template, and add this to the wrapper div
. Update the variable to genusForm.genusScientists.vars.prototype
:
{{ 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) }} |
Oh, add one other thing while we're here: I promise I'll explain all of this in a minute: data-index
set to genusForm.genusScientists|length
:
{{ form_start(genusForm) }} | |
// ... lines 2 - 23 | |
<div class="row js-genus-scientist-wrapper" | |
data-prototype="{{ form_widget(genusForm.genusScientists.vars.prototype)|e('html_attr') }}" | |
data-index="{{ genusForm.genusScientists|length }}" | |
> | |
// ... lines 28 - 41 | |
</div> | |
// ... lines 43 - 44 | |
{{ form_end(genusForm) }} |
That will count the number of embedded forms that the form has right now.
Don't touch anything else: let's refresh the page to see what this looks like... because it's kind of crazy.
Wait, oh damn, I have three "Add New Scientist" links. Make sure your link is outside of the for
loop. This link is great... but not so great that I want it three times. Oh, and fix the icon class too - get it together Ryan!
{{ form_start(genusForm) }} | |
// ... lines 2 - 23 | |
<div class="row js-genus-scientist-wrapper" | |
data-prototype="{{ form_widget(genusForm.genusScientists.vars.prototype)|e('html_attr') }}" | |
data-index="{{ genusForm.genusScientists|length }}" | |
> | |
{% for genusScientistForm in genusForm.genusScientists %} | |
// ... lines 29 - 36 | |
{% endfor %} | |
<a href="#" class="js-genus-scientist-add"> | |
<span class="fa fa-plus-circle"></span> | |
Add Another Scientist | |
</a> | |
</div> | |
// ... lines 43 - 44 | |
{{ form_end(genusForm) }} |
Refresh again. Much better!
Checking out the prototype: __name__
View the HTML source and search for wrapper to find our js-genus-scientist-wrapper
element. That big mess of characters is the prototype. Yep, it looks crazy. This is a blank version of one of these embedded forms... after being escaped with HTML entities so that it can safely live in an attribute. This is great, because we can read this in JavaScript when the user clicks "Add New Scientist".
Oh, but check out this __name__
string: it shows up in a bunch of places inside the prototype. Scroll down a little to the embedded GenusScientist
forms. If you look closely, you'll see that the fields in each of these forms have a different index number. The first is index zero, and it appears in a few places, like the name
and id
attributes. The next set of fields use one and then two.
When Symfony renders the prototype
, instead of hard coding a number there - like zero, one or two - it uses __name__
. It then expects us - in JavaScript - to change that to a unique index number, like three.
The Prototype JavaScript
Let's do it! Back on the Symfony documentation page: a lot of the JavaScript we need lives here. Find the addTagForm()
function and copy the inside of it. Back in edit.html.twig
, paste this inside our click function.
And let's make some changes. First, update $collectionHolder
to $wrapper
: that's the element that has the data-prototype
attribute. We also read the data-index
attribute... which is important because it tells us what number to use for the index. This is used to replace __name__
with that number. And then, each time we add another form, this index goes up by one.
Finally, at the very bottom: put this new sub-form onto the page: $(this)
- which is the "Add another Scientist" link, $(this).before(newForm)
:
// ... lines 1 - 2 | |
{% block javascripts %} | |
{{ parent() }} | |
<script> | |
jQuery(document).ready(function() { | |
var $wrapper = $('.js-genus-scientist-wrapper'); | |
// ... lines 9 - 17 | |
$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); | |
}); | |
}); | |
</script> | |
{% endblock %} | |
// ... lines 40 - 52 |
I think we are ready! Find your browser and refresh! Hold your breath: click "Add Another Scientist". It works! Well, the styling isn't quite right... but hey, this is a victory! And yea, we'll fix the styling later.
Add one new scientist, and hit save. Ah! It blows up! Obviously, we have a little bit more work to do.
After adding a new element, you are forced to fill it. Is there a way to add a "remove" button for new elements?