Buy

Customizing the Collection Form Prototype

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.

Customizing the Prototype!

Why does it look different, anyways? Remember the data-prototype attribute?

46 lines app/Resources/views/admin/genus/_form.html.twig
{{ 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:

46 lines app/Resources/views/admin/genus/_form.html.twig
{{ 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:

52 lines app/Resources/views/admin/genus/_form.html.twig
... 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:

52 lines app/Resources/views/admin/genus/_form.html.twig
... 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

Use that Macro!

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 %}:

52 lines app/Resources/views/admin/genus/_form.html.twig
{% 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:

52 lines app/Resources/views/admin/genus/_form.html.twig
... 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:

52 lines app/Resources/views/admin/genus/_form.html.twig
... 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.

Centralizing our JavaScript

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:

34 lines web/js/GenusAdminForm.js
(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:

34 lines web/js/GenusAdminForm.js
(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:

20 lines app/Resources/views/admin/genus/edit.html.twig
... 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!

20 lines app/Resources/views/admin/genus/new.html.twig
... 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!

Avoiding the Weird New Label

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.

54 lines app/Resources/views/admin/genus/_form.html.twig
... 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.

Leave a comment!

  • 2017-07-28 Hervé BOYER

    Hi Diego !
    Thank you very much for your answer.
    I'll trying when I'll better in JS ^^

  • 2017-07-25 Diego Aguiar

    Hey Hervé BOYER

    Do you mean something like automatically hitting the "Add another scientist" button if the Genus object doesn't have one already ? If so, you could do a check like this


    {% if genusForm.genusScientists|length == 0 %}
    // your code
    // simulate clicking that button
    {% endif %}

    I hope it helps you, if not, let me know!

    Have a nice day :)

  • 2017-07-24 Hervé BOYER

    Hi .. nice course !
    But, how to add a first field automatically if there is not already one (for example, a new ad) ? (genusScientistForm if index == 0)

  • 2017-05-25 Victor Bocharsky

    Hey Petr,

    Yes, fair point! It makes sense if you extends formLayout.html.twig only with edit.html.twig and new.html.twig templates.

    Cheers!

  • 2017-05-25 Petr Vohralík

    Would not it be better if we paste <script src="{{ asset('js/GenusAdmonForm.js') }}"></script> into formLayout.html.twig instead of two files edit.html.twig and new.html.twig?