Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Form Theming: Add an Error Icon

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

Checkout Bootstrap's form documentation. Under validation, they have a cool feature: when your field has an error, you can add a cute icon. I want a cute icon! To get it, we just need to add a has-feedback class to the div around the entire field and add the icon itself.

Right now, each field is surrounded by a div with a form-group class. How can we also add a has-feedback class to this? Answer: override the block that's responsible for rendering the row part of every field. In other words, the form_row block.

In form_div_layout.html.twig, search for the form_row block. There it is!

... lines 1 - 243
{%- block form_row -%}
<div>
{{- form_label(form) -}}
{{- form_errors(form) -}}
{{- form_widget(form) -}}
</div>
{%- endblock form_row -%}
... lines 251 - 372

But, we might be overriding this in the bootstrap theme - so check there too. Yep, we are: and this is where the form-group class comes from:

... lines 1 - 184
{% block form_row -%}
<div class="form-group{% if (not compound or force_error|default(false)) and not valid %} has-error{% endif %}">
{{- form_label(form) -}}
{{- form_widget(form) -}}
{{- form_errors(form) -}}
</div>
{%- endblock form_row %}
... lines 192 - 246

Overriding a Block

Ok! So... how can we override this? Very simple. First, copy the block. Second, go to your templates directory and create a new file called _formTheme.html.twig. The name of this isn't file is not important. And just so we know when this is working, add a class: cool-class:

{% block form_row -%}
<div class="form-group cool-class{% if (not compound or force_error|default(false)) and not valid %} has-error{% endif %}">
{{- form_label(form) -}}
{{- form_widget(form) -}}
{{- form_errors(form) -}}
</div>
{%- endblock form_row %}

Finally, we need to point Symfony to this new form theme template. And we already know where to do this: right inside config.yml. After the bootstrap template, add a new line with _formTheme.html.twig:

... lines 1 - 36
# Twig Configuration
twig:
... lines 39 - 42
form_themes:
- bootstrap_3_layout.html.twig
- _formTheme.html.twig
... lines 46 - 75

Because this is after bootstrap, its blocks will override those from bootstrap. Oh, and even though we don't have it explicitly listed here, Symfony always uses form_div_layout.html.twig as the fallback file.

Ok, go back, and refresh! Inspect any form element. There it is! Our block is now being used.

Using Variables in Blocks

And here's where things get really interesting. We need to add a class to the div, but only if this field has a validation error. Well check this out: this block is already using a few variables, like compound, force_error and valid:

{% block form_row -%}
<div class="form-group cool-class{% if (not compound or force_error|default(false)) and not valid %} has-error{% endif %}">
... lines 3 - 5
</div>
{%- endblock form_row %}

But, where are those coming from? And what other stuff can we use?

It turns out that these are the same form variables that we can override from the main, _form.html.twig template. Once you're inside of a form theme block, these become local variables.

To see this in action, call dump() with no arguments:

{% block form_row -%}
{{ dump() }}
<div class="form-group cool-class{% if (not compound or force_error|default(false)) and not valid %} has-error{% endif %}">
... lines 4 - 6
</div>
{%- endblock form_row %}

This will print all the variables we can use.

Refresh the page. Ah, now we have a big dump before every single field: revealing all of the variables we have access to. And it doesn't matter which block you're overriding: you always have access to this same, big group of variables. We can use these to only add that has-feedback class if there is an error.

Remove the dump. Then, set a new variable called showErrorIcon. Copy all of the logic from the if statement below that controls whether or not the has-error class is added and paste it here:

{% block form_row -%}
{% set showErrorIcon = (not compound or force_error|default(false)) and not valid %}
... lines 3 - 10
{%- endblock form_row %}

The most important variable is valid: if this is false, the field failed validation. Don't worry about the compound variable - we'll talk about that soon.

Next, at the end of the div, use an inline if statement so that if showErrorIcon is true, we add the has-feedback class:

{% 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 - 9
</div>
{%- endblock form_row %}

Then, to add the icon, add that same if statement after printing the widget. Add a span with the necessary classes to make this an icon:

{% 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' : '' }}">
{{- form_label(form) -}}
{{- form_widget(form) -}}
{% if showErrorIcon %}
<span class="glyphicon glyphicon-remove form-control-feedback" aria-hidden="true"></span>
{% endif %}
{{- form_errors(form) -}}
</div>
{%- endblock form_row %}

Ok, time to try it. Refresh! There's nothing yet, but there also aren't any validation errors. Empty the name field and submit. Our beautiful "X"!

But now, set the Subfamily field to "Select a Subfamily" and submit. Ok, the drop-down looks a little funny - the "X" is on top of the arrow. In fact, the Bootstrap docs warn you about this: this icon should only be added to text fields. And other fields, like checkboxes, will look even worse!

So, it's time to get a little smarter, and only add the cute icon to text fields.

Leave a comment!

6
Login or Register to join the conversation
Default user avatar

Hey, can anybody tell me is there a way to pass direct unconventional params to form_row
{{ form_row(form.fullName, {'iconClass': 'cat'}) }}
{{ form_row(form.fullName, {'iconClass': 'dog'}) }}

and then inside row just read?

I see iconClass in dump() while debug
{% block form_row %}
{{ dump() }}
but when I try to access it
{{ iconClass }}
I have an error that variable is undefined. Very strange behavior
{% endblock %}

I use symfony 4.4.

Reply

Hey Yehor!

That looks correct! I wonder if the error is actually occurring because you have a form_row() on some other fields and - since you're not passing iconClass to that form row, it's causing an error.

Try doing something like this:


{% if iconClass is defined %}
    {{ iconClass }}
{% endif %}

That should avoid a "variable is undefined" error if you don't pass this variable.

Cheers!

1 Reply
Default user avatar
Default user avatar Bananaapple | posted 5 years ago

When adding the _formTheme.html.twig to config.yml I had to set a relative path to the file from the views folder so admin/genus/_formTheme.html.twig

I am using Symfony 3.1.4 so just the version downloaded from this tutorial.

What am I doing wrong / different to cause this change in behaviour?

Reply

Hey Bananaapple

Yeah, you have to do that if your form theme does not live at the root of "app/Resources/views"

Have a nice day

Reply
Default user avatar
Default user avatar Nikolay | posted 5 years ago

I have some problem.

In EDIT form, if I'm choice option 'Select subfamily' (placeholder) in list of Subfamily, Symfony will show error:

Type error: Argument 1 passed to AppBundle\Entity\Genus::setSubFamily() must be an instance of AppBundle\Entity\SubFamily, null given, called in D:\OpenServer\domains\aquanote.local\vendor\symfony\symfony\src\Symfony\Component\PropertyAccess\PropertyAccessor.php on line 636

In ADD form all work fine - correctly handles the wrong choice and displays under SELECT field the error message.

Reply

Hey Nikolay!

This is happening because you are choosing the placeholder option, which is not a SubFamily, and since your setSubFamily() method does not allow null to be passed, it explodes. So, you have two choices, allow passing null or validate the SubFamily field.

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