Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Complex Blocks & the parent() Function

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 $10.00

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

Login Subscribe

We've just added an X glyphicon to every field that has an error...

{% block form_row -%}
{% set showErrorIcon = (not compound or force_error|default(false)) and not valid %}
<div class="form-group {% if (not compound or force_error|default(false)) and not valid %} has-error{% endif %}{{ showErrorIcon ? ' has-feedback' : '' }}">
... lines 4 - 5
{% if showErrorIcon %}
<span class="glyphicon glyphicon-remove form-control-feedback" aria-hidden="true"></span>
{% endif %}
... line 9
</div>
{%- endblock form_row %}

And then found out that we really only want to add this to input fields.

How can we do that? I know that this is a text type. So maybe, instead of adding the icon to form_row, we could override the text_widget block and add it there. That would only affect text fields.

Go into form_div_layout.html.twig and look for text_widget. Woh! It's not here! That means Symfony must be using form_widget. That block does exist:

... lines 1 - 2
{%- block form_widget -%}
{% if compound %}
{{- block('form_widget_compound') -}}
{% else %}
{{- block('form_widget_simple') -}}
{% endif %}
{%- endblock form_widget -%}
... lines 10 - 372

Remember that compound variable I refused to explain before. Well, here it is again! We normally think of a field as just, well, a field: like a text box, or a select element. But sometimes, a field is actually a collection of sub-fields. An easy example is Symfony's DateType, which by default renders as 3 select elements for year, month and day. In that case, the DateType is said to be compound: it's just a wrapper for its three child fields.

In our form, all of our fields right now are simple: so, not compound. The block() function says:

Hey! Go render this other block called form_widget_simple.

After following all of this, it turns out that if we want to override the text widget, we need to override form_widget_simple:

... lines 1 - 10
{%- block form_widget_simple -%}
{%- set type = type|default('text') -%}
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
{%- endblock form_widget_simple -%}
... lines 15 - 372

In fact, all input fields - like the number field, search field or URL field - use this same block.

Ok, let's override it! But wait - check to see if it's in the Bootstrap template first:

... lines 1 - 4
{% block form_widget_simple -%}
{% if type is not defined or type not in ['file', 'hidden'] %}
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%}
{% endif %}
{{- parent() -}}
{%- endblock form_widget_simple %}
... lines 11 - 246

It is! Copy that version, and paste it into _formTheme.html.twig.

The Craziness of Twig in Form Themes

Now, check out this logic: form theme templates will have some of the craziest Twig code you'll ever see! In normal words, this says:

If type is not defined or file does not equal type, add a new form-control class.

To make this happen, it uses the attr variable that we were playing with before and merges in the new class, adding a space in case there was already a class.

Stealing Parent Blocks with use

Before we make any changes, go back and refresh. Woh! It doesn't work - that's surprising. The problem is this parent() call:

... lines 1 - 4
{% block form_widget_simple -%}
... lines 6 - 8
{{- parent() -}}
{%- endblock form_widget_simple %}
... lines 11 - 246

We understand that in normal Twig templates, you can override parent blocks and use the parent() function. But check this out: our template does not extend any Twig template... and we don't want it to! For reasons that honestly aren't very important, a form theme template should never extend anything.

But wait, then, how did this code work in the Bootstrap template? Go look: at the top, it has a use for form_div_layout.html.twig:

{% use "form_div_layout.html.twig" %}
... lines 2 - 246

The use says:

I don't actually want to extend this other template. But, please allow me to call the parent() function as if I were extending it.

The use statement is an awesome Twig feature that allows you to just, grab and use random blocks from a different template. It's advanced, but now it's in your toolkit! Go you!

At the top of our template, use 'bootstrap_3_layout.html.twig':

{% use "bootstrap_3_layout.html.twig" %}
... lines 2 - 21

And as soon as we do that, life is good.

And actually, we don't need this logic anymore: that's done in the parent() block:

... lines 1 - 14
{% block form_widget_simple -%}
{% if type is not defined or type not in ['file', 'hidden'] %}
{% endif %}
{{- parent() -}}
{%- endblock form_widget_simple %}

If you refresh, everything still looks great.

The Error Icon in the Widget

Finally, we can move the icon to this block. First, keep that showErrorIcon variable: we need that to add the has-feedback class. But copy it and move it down into form_widget_simple, inside the if statement... because, it turns out, we probably also don't want to show the error icon if this is a file upload field:

... lines 1 - 11
{% block form_widget_simple -%}
... line 13
{% if type is not defined or type not in ['file', 'hidden'] %}
{# show error icon for these types #}
{% set showErrorIcon = (not compound or force_error|default(false)) and not valid %}
{% endif %}
{{- parent() -}}
... lines 19 - 21
{%- endblock form_widget_simple %}

Above, set showErrorIcon to false by default:

... lines 1 - 11
{% block form_widget_simple -%}
{% set showErrorIcon = false %}
... lines 14 - 21
{%- endblock form_widget_simple %}

Finally, copy the span icon code, remove it, and paste it right after the parent call, to put this after the widget.

That should do it! Resubmit the form! Got it! One fancy error icon on the name text field, and zero fancy error icons on the select field.

In a nutshell, form theming means:

  • (A) finding the right block to override and then;
  • (B) leveraging your variables to do cool stuff.

Next, we'll add a missing feature to Symfony: field help text.

Leave a comment!

2
Login or Register to join the conversation
Mirha M. Avatar
Mirha M. Avatar Mirha M. | posted 4 years ago | edited

Hello! :-)

I was wondering how do I dump type?

And is the type that is being referred to the one under:

FormView.vars.errors.form.config```


Thank you ..
Reply

Hey Mirha M.

You could override "form_widget" block and do dump(type) or just do dump() so you can see all the available variables on that scope.

And is the type that is being referred to the one under:...

Nope, the type is the name of the FieldType you choosed. If you are curious, go open the "TextType" class, then, you will see a method named "getBlockPrefix", there is where the type value comes from.

Cheers!

Reply
Cat in space

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

This tutorial is built on Symfony 3 but form theming hasn't changed much in Symfony 4 and Symfony 5. Other than some path differences - this tutorial should work fine.

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