Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

So, 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.

Leave a comment!

24
Login or Register to join the conversation
Lluís F. Avatar
Lluís F. Avatar Lluís F. | posted 1 year ago

After adding a new element, you are forced to fill it. Is there a way to add a "remove" button for new elements?

Reply

Hey Lluís F.

That's totally doable and Ryan shows how to do it in this chapter https://symfonycasts.com/sc...

Cheers!

Reply
Lluís F. Avatar
Lluís F. Avatar Lluís F. | MolloKhan | posted 1 year ago

I didn't express myself. I used this tutorial to make a code where "add new element" inserts a subform with mandatory fields. When the subform is inserted, you cannot left it blank nor can remove it. The "delete" button don't appear in it. I tried adding it at bulidForm() but the jQuery event is not associated to it (because it is added after "jQuery(document).ready()").

Reply

What you can do is to bind the click event for the remove action everytime you add a new subform, or, you can bind the click action to a parent element that's present at the moment of page load. This SO post may be useful https://stackoverflow.com/a...

Cheers!

Reply
Juan-Etxenike Avatar
Juan-Etxenike Avatar Juan-Etxenike | posted 2 years ago

Thanks for such great tutorial. I have borrowed many of the ideas presented here for a project I am working on. Now, I have a question. What if any of the fields provided by the ChoiceType embedded forms, has, for example, as it is in my case a datepicker Jquery property attached to them. How can I make such property available now?

Reply

Hey Juan E.

I think it should work as expected as long as you attach the jQuery function to a parent element of the wrapper of your form fields. Perhaps this SO post can help https://stackoverflow.com/a...

Cheers!

Reply
Ruben Avatar

Hi, I bought this tutorial and it has helped me a lot, thanks, but I need one more step for my project and I don't know how to adapt the tutorial code. You load the existing DB users into the Select when you create new relationships. That's fine, but my application also needs that if there is no user, allow it to be written in that field. That is, to create users on the fly in the same form, in case they don't exist yet in the database, and also the relationship. In other words, I need to embed the entire User form, with all its fields, inside this form, not just the mail. How could I do it?

Edit: I'm working with Symfony 4

Reply

Hey Ruben

I'm glad to hear that you are liking our tutorials :)

About your problem, you can embed a whole form inside another one easily. Check out the docs: https://symfony.com/doc/cur...
or you can learn A LOT more if you follow our Symfony Forms tutorials: https://symfonycasts.com/sc...

Cheers!

Reply
Dirk Avatar

I thought I had everything working, but then I noticed when I remove and add in a collection in the same form, the __name__ will get overridden or doubles and creates a big mess. On the symfony.com website I read "// And store it, the length cannot be used if deleting widgets is allowed". Does this mean deleting and adding at the same time cannot be done?

Reply

Hey Dirk!

Hmm. I'm not 100% sure, but I'm *pretty* sure that adding and removing all on the same form was no problem when we finished this tutorial. Are you following this tutorial or the symfony.com docs? And, I can't find the "And store it, the lenth cannot be used..." text either here on SymfonyCasts or on the docs (https://symfony.com/doc/cur... - where is that comment?

Cheers!

Reply
Dirk Avatar

Thanks for getting back to me. The comment I mentioned is on: https://symfony.com/doc/cur....

I have been mixing both your tutorial and the documentation to try and find a solution. Maybe my explaining was not too great; what my issue is, is that when I add some, remove some, then submit, have validation errors, re-add and delete, and again have a validation error things get messy. For first time it 'could' go wrong I have added a hidden input field which counts how many items where added to the collection (without caring about any that are removed), and that value is submitted together with the form. So when I have validation errors I still can add/delete new items without writing over older ones. There is an issue though on validation errors that show on the wrong fields. My id's could be 2,4,7,8 and according to the validation path this could be completely different (which is why errors show up on the wrong field (or are completely hidden). Very frustrating. My guess is other people should also have this problem and I posted a question about it on StackOverflow (https://stackoverflow.com/q..., but no responses. Maybe my explanation is not too clear...

Reply

Hey Dirk!

Hmm. So I just tried the code from the finish part of this tutorial. And even that code, I admit, doesn't work quite right when you're adding and deleting items. For the counter, we always start with the first available integer after the form loads. So, if we have 7 items when the form loads (index 0 - 6), adding a new item (even if you've deleted some already) starts at 7. However, when you delete one, yes, the validation seems to get confused. Internally, by using the form profiler after submitting, I can see the problem. I have start with indexes 0-6, delete 6, then add 7, when I submit, the form DOES submit the new entry with the index 7. However, at some point while processing, this becomes index 6. Basically, at some point during the submit process, the submitted data is "reindex" - so 0, 1, 2, 3, 3, 4, 5, 7 becomes 0, 1, 2, 3, 4, 5, 6.

Unfortunately, I think this just may be a known limitation - https://github.com/symfony/.... The CollectionType is really complex, and if you push it far enough, I admit, it just doesn't work right :/. If you can't get your form working like you need to, you'll need to back up and handle it with JavaScript and AJAX (and not the form component) - this is what I tend to do anyways.

That's not a great answer - but I hope it helps.

Cheers!

Reply
Default user avatar
Default user avatar Mark Ernst | posted 4 years ago

With the help of the tutorial back on Symfony.com and ultimately following this one, I've got my parent/child setup completely done, but with 1 small problem. I've got a Record with SubRecords. They are meant to break down a specific mutation (like 100) and divide it into subs (so 3 subrecords of 25, 70 and 5). When I inject a fake row in the database, the prototype renders just fine (as one child is rendered thus a prototype is available), but when there are no children (no database rows), the prototype is empty except for the base HTML. So, no forms of any kind. The only solution I found is to add a child before the form rendering, which is an empty object.

Is there a better way to achieve this? I tried setting 'data' to 'new Record' but to no avail...

Reply

Hey Mark Ernst

Interesting... it sounds like a pretty serious setup! I think I might not *totally* understand the setup, however, because I can't see why the prototype wouldn't render until you actually had at least one object. For example, if you download this tutorial, we edit/create a "Genus" object which has many GenusScientist (which is the collection object). If you create a new Genus object (and so it has no GenusScientist), the prototype *does* render just fine: https://pasteboard.co/HQ1dZ...

This makes me think that there's some more complex part of your setup that I don't understand yet. Let me know!

Cheers!

Reply
Default user avatar
Default user avatar Mark Ernst | weaverryan | posted 4 years ago | edited

HI weaverryan, thanks for the quick reply, awesome!

i'll explain in more detail. Basically I've got a ParentRecordType and a RecordType. Record has a ManyToOne via $parent and also has a OneToMany $children. Basically you could see this as nested categories, though the interface only goes 1 level deep for the moment.

So, naturally the Record belongs to the RecordType. The ParentRecordType is also a form but only with one field, children, which is a CollectionType. The EntityType is as expected, a RecordType which then in turn again links back to the Record. Basically I now have a form where you can supposedly "update" the parent Record's children, which are rendered in sequence.

My only fix I could get to work in order to render the prototype is as follows: https://pastebin.com/1mej6UpK
The ParentRecordType is as follows: https://pastebin.com/p1R7m3NV (note that the RecordSplitType has 2 less children because of the parent/child relationship).
Additionally, I have a template that uses form_theme to style form.children, which relates back to ParentRecordType. When I then, in turn, render the prototype, no fields but the basic markup appear. I've also tried is with inline marco's referenced from _self, but that didn't do the trick either. Did I miss something?

Reply

Hey Mark Ernst!

Sorry for the slow reply - it was SymfonyCon conference week :). Ok, I don't see anything with your setup that really looks wrong or missing (unfortunately). But, I might be able to help you debug if you want to keep looking into it. The "prototype" is actually a "fake field" added by the CollectionType - it literally builds a mini-field and "attaches" itself to the form. This happens in CollectionType - https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Form/Extension/Core/Type/CollectionType.php#L39. This is rendered later (as you know) via the "prototype" being accessible as a variable (so you can literally say form_row(form.children.vars.prototype) if you wanted to.

What worries/confuses me is that you say:

then, in turn, render the prototype, no fields but the basic markup appear

If the prototype is not absolutely empty, but contains some markup, that makes me think that the prototype IS working... but that there is some extra "level" in the form that I still can't see. What is the "top-level" form that's being rendered? Is it the ParentRecordType or is there even another type above that?

Anyways, since you've found a workaround, I don't know how much you care - but if you want to keep debugging, I'm happy to help how I can :). If you're able to push a mini-project with ALL of the code - that's really the best way so I can play around with things and see exactly what's going on.

Cheers!

Reply
Default user avatar

No problem, busy days here as well!

The ParentRecordType is the actual form being handled in the controller and that's the one that is being rendered. I simply iterate all Record.children in the template which in turn renders templates (such as the prototype).
The workaround works fine, but I do care very much if this is the right way, as it just feels dirty and wrong. :p
I'll set up a mini-project on my github for you to check out and fiddle around with if that's OK. I'll get back to you on that. I'll share it here so others may benefit from it as well.

Reply

Hey Mark Ernst!

I'll set up a mini-project on my github for you to check out and fiddle around with if that's OK. I'll get back to you on that. I'll share it here so others may benefit from it as well.

That's really awesome - that would be great :).

The ONE thing I can think of is this: the "prototype" will be attached to the form.children field (in Twig land). Because you are looping over these to render each one, when there are NO children, this field is basically not rendered! The difference is this:


{# would loop over each "child" and render... but I think would also mean the prototype is always rendered #}
{{ form_row(form.children) }}

{# here, you're technically never rendering the "form.children" field - just what's inside of it #}
{# I'm actually a little surprised that you EVER see the prototype #}
{% for child in form.children %}
    {{ form_row(child);
{% endfor %}

Basically, if you literally render the "CollectionType" field itself, then it renders with the data-prototype attribute. But if you just render its children, then you are responsible for rendering the prototype... somewhere - for example:


<div data-prototype="{{ form_widget(form.children.vars.prototype)|e('html_attr') }}">
</div>

But, I could be way off - depends on how you're rendering things right now, of course :p.

Cheers!

Reply

Happy to find such an amazing Tuto talking about CollectionType, as usual <3
Have somes question wish I got responses:
As I see you worked with macros, this has two benefits first rending the form many times via the macro block, second, customizing the collection prototype.
But there's other trick, can do the same job, I saw some article such http://symfony.com/doc/mast... talking about, how to override the prototype using twig themes,

+ But, when we use themes? And when we use macros?

Reply

Hey ahmedbhs

Themes are great when you want/need forms with the same layout, it will reduce you a lot of the work, and even if you need different layouts, because you can create multiple Form themes, and then just choose the one you need

Have a nice day!

1 Reply
Default user avatar
Default user avatar Kjell K. | posted 5 years ago

Hi,

Could someone think of any reason why the prototype object contains double the number of items I put in it?
Whenever I click the add collection button, two sets are inserted.

Any help would be much apprechiated

Reply

Hi Kjell,

Hm, probably you just made a misprint somewhere or doubled some html/js code, could you double check and compare your code with ours one more time? Press "Download" button in the right top corner of any video page of this course, then click "Course Code". In finish/ folder you'll find the finish code for this tutorial. It's difficult to say more about what could be a problem based on your question, sorry.

Cheers

Reply
Default user avatar

Cheers, I discovered that. I did not double it up. But the java scripts were put inline in the base.html. Ended up moving the js snipps to separate files and including, and that solved the problem. Finally got it working, after too many hours of banging my head against the wall. And worse still, I still don't know why it solved the problem. I must admit I did not follow the complete course, only stole the parts I needed. But thank you very much for your prompt reply, it is much appreciated!

Reply

Ah, anyway I'm glad you got it working, well done!

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is built on Symfony 3, but most of the concepts apply just fine to newer versions of Symfony.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "symfony/symfony": "3.4.*", // v3.4.49
        "doctrine/orm": "^2.5", // 2.7.5
        "doctrine/doctrine-bundle": "^1.6", // 1.12.13
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.23.0
        "sensio/distribution-bundle": "^5.0", // v5.0.25
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.4
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.47
        "nelmio/alice": "^2.1", // v2.3.6
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice