Buy

Rendering Fields Manually

Finally, let's look at the Swiss Army knife of form rendering: instead of using the form-rendering functions, we'll build the field entirely by hand.

For example, suppose we need to do something crazy with the "year" drop-down field. That's fine! We'll still render the label and errors normally, but let's handle the widget ourselves. Yep, I literally mean: create a select tag and start filling in the details.

The first detail is the id attribute. Every field has a unique id, which ties that field to its label. And this is where form variables help us out big.

Referencing Field Variables Directly

Go back into the Form tab of the web profiler and click the year field. There are a lot of variables, but there are a few that are especially important, like id and full_name, which normally becomes the name attribute.

In your template, reference the id variable with: genusForm.firstDiscoveredAt.year.vars.id. Repeat that for the name attribute set to genusForm.firstDiscoveredAt.year.vars.full_name:

35 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 20
{{ form_label(genusForm.firstDiscoveredAt.year) }}
<select id="{{ genusForm.firstDiscoveredAt.year.vars.id }}"
name="{{ genusForm.firstDiscoveredAt.year.vars.full_name }}">
... lines 24 - 26
</select>
{{ form_errors(genusForm.firstDiscoveredAt.year) }}
{{ form_row(genusForm.firstDiscoveredAt.month) }}
{{ form_row(genusForm.firstDiscoveredAt.day) }}
... lines 32 - 33
{{ form_end(genusForm) }}

Now that we understand the FormView tree and how variables are stored, this actually makes sense.

Printing the Options

Next, what about the options that should go inside the select tag? Head back to the web profiler to see which variable might help us. Ah, here's one called choices, and each item is a ChoiceView object. Use the Shift+Shift shortcut to open that file from Symfony:

65 lines vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php
... lines 1 - 11
namespace Symfony\Component\Form\ChoiceList\View;
... lines 13 - 18
class ChoiceView
{
/**
* The label displayed to humans.
*
* @var string
*/
public $label;
/**
* The view representation of the choice.
*
* @var string
*/
public $value;
/**
* The original choice value.
*
* @var mixed
*/
public $data;
/**
* Additional attributes for the HTML tag.
*
* @var array
*/
public $attr;
/**
* Creates a new choice view.
*
* @param mixed $data The original choice
* @param string $value The view representation of the choice
* @param string $label The label displayed to humans
* @param array $attr Additional attributes for the HTML tag
*/
public function __construct($data, $value, $label, array $attr = array())
{
$this->data = $data;
$this->value = $value;
$this->label = $label;
$this->attr = $attr;
}
}

Cool! Each ChoiceView is a simple object, with a public label property and a public value property:

65 lines vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/View/ChoiceView.php
... lines 1 - 18
class ChoiceView
{
/**
* The label displayed to humans.
*
* @var string
*/
public $label;
/**
* The view representation of the choice.
*
* @var string
*/
public $value;
... lines 34 - 63
}

That's exactly what we need.

Add a loop: for choice in genusForm.firstDiscoveredAt.year.vars.choices:

35 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 20
{{ form_label(genusForm.firstDiscoveredAt.year) }}
<select id="{{ genusForm.firstDiscoveredAt.year.vars.id }}"
name="{{ genusForm.firstDiscoveredAt.year.vars.full_name }}">
{% for choice in genusForm.firstDiscoveredAt.year.vars.choices %}
... line 25
{% endfor %}
</select>
{{ form_errors(genusForm.firstDiscoveredAt.year) }}
{{ form_row(genusForm.firstDiscoveredAt.month) }}
{{ form_row(genusForm.firstDiscoveredAt.day) }}
... lines 32 - 33
{{ form_end(genusForm) }}

Inside, add <option value=""> then print choice.value.

We also need to know if this option should be currently selected. We can do that by comparing the value to the data variable that's attached to the year field. Why not do this in one big giant line: choice.value == genusForm.firstDiscoveredAt.year.vars.data. Wow. Then, ? ' selected' or empty quotes. Finally, for the option text, use choice.label:

35 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 20
{{ form_label(genusForm.firstDiscoveredAt.year) }}
<select id="{{ genusForm.firstDiscoveredAt.year.vars.id }}"
name="{{ genusForm.firstDiscoveredAt.year.vars.full_name }}">
{% for choice in genusForm.firstDiscoveredAt.year.vars.choices %}
<option value="{{ choice.value }}" {{ choice.value == genusForm.firstDiscoveredAt.year.vars.data ? 'selected' : '' }}>{{ choice.label }}</option>
{% endfor %}
</select>
{{ form_errors(genusForm.firstDiscoveredAt.year) }}
{{ form_row(genusForm.firstDiscoveredAt.month) }}
{{ form_row(genusForm.firstDiscoveredAt.day) }}
... lines 32 - 33
{{ form_end(genusForm) }}

That's it! Go back to your browser, then refresh. Ah, error!

That's me being careless: the sub-field is called year, not years:

35 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 21
<select id="{{ genusForm.firstDiscoveredAt.year.vars.id }}"
name="{{ genusForm.firstDiscoveredAt.year.vars.full_name }}">
{% for choice in genusForm.firstDiscoveredAt.year.vars.choices %}
<option value="{{ choice.value }}" {{ choice.value == genusForm.firstDiscoveredAt.year.vars.data ? 'selected' : '' }}>{{ choice.label }}</option>
{% endfor %}
</select>
... lines 28 - 33
{{ form_end(genusForm) }}

Refresh again.

It works! It's not styled because we've taken complete control of rendering it. But you can see the errors, and the options look correct. Cool!

Marking Fields as Rendered

So, we're done! Wait... except for this random field at the bottom of my form. What the heck!? That's my year field! What's going on?

See that form_end() at the bottom of our form?

35 lines app/Resources/views/admin/genus/_form.html.twig
... lines 1 - 33
{{ form_end(genusForm) }}

Remember how it renders any field that we forgot to render? Well, now it thinks that we forgot to render the year field. The nerve!

So, could we just tell it that the field was actually rendered? Yep, and the code is both simple and strange. Use a rare do tag from Twig and say genusForm.firstDiscoveredAt.year.setRendered():

36 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 28
{% do genusForm.firstDiscoveredAt.year.setRendered() %}
... lines 30 - 34
{{ form_end(genusForm) }}

Whaaaat? Well, every field is a FormView object. And if you open that class, it has a setRendered() method!

163 lines vendor/symfony/symfony/src/Symfony/Component/Form/FormView.php
... lines 1 - 18
class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
{
... lines 21 - 86
/**
* Marks the view as rendered.
*
* @return FormView The view object
*/
public function setRendered()
{
$this->rendered = true;
return $this;
}
... lines 98 - 161
}

And by calling it, we're saying:

Yo, we rendered this already. So, you know, don't try to render it again.

Refresh now! Whoops! Another Ryan mistake - make sure your variable is genusForm, not genus:

36 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 28
{% do genusForm.firstDiscoveredAt.year.setRendered() %}
... lines 30 - 34
{{ form_end(genusForm) }}

Now that extra field is gone.

Wrap it Up!

Congrats team: you have the power to render your forms in whatever crazy, insane, creative way you want! But with power, comes great responsibility. I'll delete all the code we just added and go back to simply rendering genusForm.firstDiscoveredAt:

24 lines app/Resources/views/admin/genus/_form.html.twig
{{ form_start(genusForm) }}
... lines 2 - 20
{{ form_row(genusForm.firstDiscoveredAt) }}
... line 22
{{ form_end(genusForm) }}

54 lines src/AppBundle/Form/GenusFormType.php
... lines 1 - 15
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 21 - 38
->add('firstDiscoveredAt', DateType::class, [
'widget' => 'single_text',
'attr' => ['class' => 'js-datepicker'],
'html5' => false,
])
;
}
... lines 46 - 52
}

Don't use your new skills unless you actually need to.

Ok guys, that's it! If you still have some questions, or want to tell me about something really cool you did, or share vacation photos, whatever, you can do it in the comments - it's always great to hear from you.

All right guys, see you next time.

Leave a comment!

  • 2017-08-07 Victor Bocharsky

    Hey Juan,

    Nice catch in such a long string! Thank you, fixed in https://github.com/knpunive...

    Cheers!

  • 2017-08-05 Juan Carlos Migliavacca

    Hi Ryan, first id like to say your videos are great!. Also, i think that there is a mistake in the if statement on line 25 app/Resources/views/admin/genus/_form.html.twig. (choice.value == genusForm.firstDiscoveredAt... ? 'selected' : '' )

  • 2017-03-24 Kebabra Abdessamad

    just finished the course and i love it, thanks man :) already feeling dangerous hhhh

  • 2016-10-27 St├ęphane

    Hello Ryan,
    Thank for this cool stuff. Yours tutorials are very interesting.