Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Form Theming a Single Field

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

The last thing we need to do is fix this "agree to terms" checkbox. It doesn't look that bad... but this markup is not the markup that we had before.

This fix for this is... interesting. We want to override how the form_row is rendered... but only for this one field - not for everything. Sure, we could override the checkbox_row block... because this is the only checkbox on this form. But... could we get even more specific? Can we create a form theme block that only applies to a single field? Totally!

Go back and open the web debug toolbar for the form system. Click on the agreeTerms field and scroll down to the "View Variables". A few minutes ago we looked at this block_prefixes variable. When you render the "row" for a field, Symfony will first look for a block that starts with _user_registration_form_agreeTerms. So, _user_registration_form_agreedTerms_row. If it doesn't find that, which of course it will not, it falls back to the other prefixes, and eventually uses form_row.

Creating the Form Theme Block

To customize just this one field, copy that long block name and use it to create a new{% block _user_registration_form_agreeTerms_row %}, then {% endblock %}. Inside, let's literally copy the old HTML and paste.

Try it! Find the main browser tab and refresh. Whoops!

A template that extends another cannot include content outside Twig blocks.

Yep, I pasted that in the wrong spot. Let's move it into the block. Come back and try that again. Yea! The checkbox moved back into place. Yep, the markup is exactly what we just pasted in.

Customizing with Variables

This is nice... but it's totally hardcoded! For example, if there's a validation error, it would not show up! No problem! Remember all of those variables we have access to inside form theme blocks? Let's put those to use!

First, inside, call {{ form_errors(form) }} to make sure any validation errors show up. I can also call form_help() if I wanted to, but we're not using that feature on this field.

Second: this name="_terms" is a problem because the form is expecting a different name. And so, this field won't process correctly. Replace this with the very handy full_name variable.

... lines 1 - 17
{% block _user_registration_form_agreeTerms_row %}
<div class="checkbox mb-3">
{{ form_errors(form) }}
<label>
<input type="checkbox" name="{{ full_name }}" required> Agree to terms I for sure read
</label>
</div>
{% endblock %}
... lines 26 - 78

And... I think that's all I care about! Yes, we could get fancier, like using the id variable... if we cared. Or, we could use the errors variable to print a special error class if errors is not empty. It's all up to you.

The point is: get as fancy as your situation requires. Try the page one more time. It looks good and it will play nice with our form.

Next: let's learn how to create our own, totally custom field type! We'll eventually use it to create a special email text box with autocompletion to replace our author select drop-down.

Leave a comment!

40
Login or Register to join the conversation
Ruslan Avatar

Hi,
Could you clarify this things:
I've tried to do the same manipulation with theme for my SF6 project.
But I have another structure :
base.html.twig
index.html.twig -> include _register.html.twig <- here is a form (theme is in _register.html.twig)

If I pass from controller var formRegistration, I get error for template variables like help, attr (all variables from template)
But if I pass from controller form variable with name "form" all works :)

I would like to have form rendering separately from main template because I plan to use Stimulus and get html for that form (_register.html.twig) .
Is it possible? Or if I make "include" I haven't possibility to use theme inside template.

Reply

Hey Ruslan

I'm afraid but I don't quite understand your problem. I believe you have a problem with variable names. If you just need to rename a variable inside a Twig template, you can use the {% set %} operator.
I hope it helps. Cheers!

Reply
Shaun T. Avatar
Shaun T. Avatar Shaun T. | posted 2 years ago

Is it possible to override the bootstrap styles here? I need to replace the form-check form-check-inline classes that is placed around each radio input.

<div class="form-check form-check-inline">        
<input type="radio" id="feedback_answer_1_0" name="feedback[answer_1]" required="required" class="form-check-input" value="1">
<label class="form-check-label required" for="feedback_answer_1_0">1</label>
</div>

This is my form type:

$builder->add('answer_1, ChoiceType::class, 'choices' => [
1 => 1,
2 => 2,
3 => 3,
4 => 4,
5 => 5,
6 => 6,
7 => 7,
8 => 8,
9 => 9,
10 => 10
],
'label' => $i,
'expanded' => true,
'label_attr' => [
'class' => 'radio-inline'
],
'constraints' => [
new NotBlank(),
]);

// config/packages/twig.yaml
twig:
default_path: '%kernel.project_dir%/templates'
debug: '%kernel.debug%'
strict_variables: '%kernel.debug%'
exception_controller: null
form_themes: ['bootstrap_4_layout.html.twig']
Reply

Hey Shaun T.!

Yep, it is possible! You would need a custom form theme for that. There is an attr option, but that would only allow you to control the classes on the actual input, not its wrapper. We have a tutorial all about that - it's on an outdated version of Symfony, but the form theming (other than the templates directory) should still basically be the same: https://symfonycasts.com/sc...

In your case, it looks like you would need to override the checkbox_widget block - https://github.com/symfony/... - I would duplicate that block, then just change the one line you need. You could use this custom form theme globally (by adding it to twig.yaml) or just for this *one* form.

Cheers!

Reply
Shaun T. Avatar

Hey Ryan, thanks for your help, that video is super useful!

The github link wasn't working for me, but I copied the checkbox_widget block from vendor/symfony/twig-bridge/Resources/views/Form/bootstrap_4_layout.html.twig in my project, and pasted it into my form.

But it doesnt seem to be rendering any form elements now...

</div>
{%- set parent_label_class = parent_label_class|default(i.vars.label_attr.class|default('')) -%}
{%- if 'radio-custom' in parent_label_class -%}
{%- set attr = i.vars.attr|merge({class: (i.vars.attr.class|default('') ~ ' custom-control-input')|trim}) -%}
<div class="custom-control custom-radio{{ 'radio-inline' in parent_label_class ? ' custom-control-inline' }}">
{{- form_label(i.vars.form, null, { widget: parent() }) -}}
</div>
{%- else -%}
{%- set attr = i.vars.attr|merge({class: (i.vars.attr.class|default('') ~ ' form-check-input')|trim}) -%}
<div class="form-check{{ 'radio-inline' in parent_label_class ? ' form-check-inline' }}">
{{- form_label(i.vars.form, null, { widget: parent() }) -}}
</div>
{%- endif -%}
<div>

Reply

Hey Shaun T.

I believe Ryan meant this link https://github.com/symfony/...

You need to copy the whole block definition into your form or create a custom form layout. You can learn a bit more about form layouts here https://symfony.com/doc/cur...

I hope it helps. Cheers!

Reply
Default user avatar
Default user avatar Justin Finkelstein | posted 2 years ago

OK, so here's a question for you: I have two Choice Type fields on my form and I want to "decorate" the options by overriding the block choice_widget_options differently for each field.

I can't declare choice_widget_options twice, so what would be a workaround to allow be to have one choice_widget_options for one field and another for a different field, on the same template?

Reply

Hey Justin,

Hm, yeah, usually the field prototype is the same for the whole form :) Well, I believe you can set a specific class for one field and in the choice_widget_options you're overriding you can get access to that class and see if it contains the specific class or no. So, with a simple if you can get a different view for different choice field.

Well, as an alternative solution, and if you render your form manually field by field, you can try to put those choice fields rendering in a different sub-templates and include them in the form. Like, you have a template where you render the form, and 2 more sub-templates each renders its own choice field. And include those templates in the main template inside your form. This way I hope you would be able to override that choice_widget_options differently for those different sub templates. But fairly speaking it's a wild guess, I've never don't it before, so I'm not sure it will work.

Anyway, I think the first option I give you is easier and should definitely work :) Or yeah, see Brandon's solution below if you can do that field difference with styles :)

Cheers!

Reply
Default user avatar
Default user avatar Justin Finkelstein | victor | posted 2 years ago

Hi Victor

Thanks for your reply. I implemented the sub-templates approach (see my answer to Brandon's below) which is quite efficient and reasonably elegant.

Would you mind providing an example, though, for clarity?

Cheers!

Reply

Hey Justin,

Well done! I'm happy you were able to get it working. Sure, if you want to share your solution - feel free to do it.

Cheers!

Reply
Brandon Avatar

Justin, I'm not part of Symfony but have been working through a similar problem, in your form builder, are you able to add 'attr' => ['class' => 'choice field one'] to the first and 'attr' => ['class' => 'choice field two'] to the second, by giving them each a different class are you able to style them separately in a style sheet?

Reply
Default user avatar
Default user avatar Justin Finkelstein | Brandon | posted 2 years ago

Hey Brandon

So I found a way to do this, with some help from others. What I was able to do was override the standard Twig template for a Choice type. Here's the code:


{% form_theme form.categories _self %}
{% block _search_form_categories_widget %}
<select {{="" block('widget_attributes')="" }}{%="" if="" multiple="" %}="" multiple="multiple" {%="" endif="" %}="">
{%- set options = choices -%}
{% for group_label, choice in options %}
<option value="{{ choice.value }}" {%="" if="" choice="" is="" selectedchoice(value)="" %}="" selected="selected" {%="" endif="" %}="">{{ choice.label }}{% if choice.value in aggs.categories|keys %} ({{ aggs.categories[choice.value] }}){% else %} (0){% endif %}</option>
{% endfor %}
</select>
{% endblock %}

Here's how this works:
# form_theme form.categories _self tells Twig that I'm defining a theme inside the current template that's meant to be applied to form.categories (the form field I'm styling)
# block _search_form_categories_widget says that I want to override the default template for my search form field "categories"

I then use this to populate the contents of the drop-down with the original labels, adding in the aggregations data I'm getting from Elasticsearch.

The only catch is that I have to use a global Twig variable to contain the agg data as this template doesn't have access to the main template variables that are passed in by the controller.

Hope this helps!

Reply

Hey Justin,

Thank you for sharing your solution with others! Well done!

Cheers!

Reply
Brandon Avatar
Brandon Avatar Brandon | posted 2 years ago

I'm using CollectionType which has two fields for an order form, quantity and description, is there a way to have quantity and description render next to each other rather than on two separate lines? [ Quantity ] [ Description ] I understand how to add a class to each field, but in using css display inline or anything else I've tried they never get next to each other. Is this because they are coming from my controller as shown on https://symfony.com/doc/cur... That is the tutorial I've followed but every example I find has one field and not multiple. Any help would be much appreciated.

Reply

Hey Brandon

You may want to use a different form theme then, probably the bootstrap_4_horizontal_layout.html.twig you can see the full list here https://symfony.com/doc/cur...

Or, you can handle the rendering completely by yourself, check out this docs to understand more about how to do it https://symfony.com/doc/cur...

Or, you can watch our tutorial about Forms Rendering. It's based on Symfony3 but the main concepts are still relevant https://symfonycasts.com/sc...

Cheers!

Reply
Brandon Avatar

Diego,
I think I have it figured out now, I have the following in my template:
{{ form_start(ordersForm) }}
{{ form_row(ordersForm.ordersdaterequired) }}
<table>
<tr>
<td>{{ form_widget(ordersForm.orderitems.vars.prototype.quantity) }}</td>
<td>{{ form_widget(ordersForm.orderitems.vars.prototype.description) }}</td>
</tr>
</table>
{{ form_end(ordersForm) }}
So now they are right next to each other which is what I needed, thank you.
When I use javascript to add more to the ordersForm, can I copy the entire <tr> or do I need to have jquery copy both the <td>'s?

Reply

I believe copying the entire <tr> element is the right way to do it but you can give it a try :)

Reply
Brandon Avatar

Diego, I'm still working with the data-prototype, this is my code:
<div class="js-copy-me">
<div class="col-30" data-prototype="{{ form_widget(ordersForm.orderitems.vars.prototype.quantity('html_attr')) }}">
{{ form_widget(ordersForm.orderitems.vars.prototype.quantity) }}
</div>
<div class="col-70" data-prototype="{{ form_widget(ordersForm.orderitems.vars.prototype.description('html_attr')) }}&gt;
{{ form_widget(ordersForm.orderitems.vars.prototype.description) }}
&lt;/div&gt;
&lt;/div&gt;
But I get an error the property quantity isn't found. But when I leave the data-prototype out of the containing div everything renders fine. Any suggestions?">

Reply

Hmm, that's interesting. Where do you get that error, in your Javascript or from Twig? This docs may help you out as well https://symfony.com/doc/cur...

Reply
Brandon Avatar

Diego, that doc is the exact one I have been following. I changed my javascript to something different and I have it working now, thank you! Another issue I'm having is that this order form is inside of a route that is from a job, so /home/jobs/1/orders is the route. How can I pass that jobid 1 to the database in my controller without using a form field for it? In a sense I don't want the user to have to select the job for the order each time, they are already in that jobs page. I've tried adding a hidden field, but I get the
Expected argument of type "App\Entity\Jobs or null", "string" given at property path "ordersjob".
Controller code looks like this:
* @Route("/home/jobs/{id}/orders/add", name="orders_add")
$orders = $form->getData();
$orders->setOrdersdate(new \DateTime());
$orders->setOrdersby($this->getUser());

I've also tried:
$orders->setOrdersjob($id); but I get that same string error.
Any ideas?

Reply

If you're rendering the form from the path /home/jobs/1/orders where the value 1 is the Jobs id, then, your form's action should be pointing to that path. I suppose you're processing and rendering the form *in* the same route, if that's the case, then, you don't need to set the form's action, it will use the current route by default. Being said so, your controller's route should looks like this


/**
* @Route("/home/jobs/{id}/orders/add", name="orders_add")
*/
public function orderJobAdd(Jobs $job, Request $request)
{
...
}


That's the correct way of doing it and you should't have to add a hidden field into your form

Cheers!

Reply
Brandon Avatar

Diego, that is correct, I am processing the form in the same route. I'm not setting the forms action, but when I submit the form, it says that the variable ordersjob is null, and it can't be because it is relation. I'm sorry that I didn't copy my full route before which is this:
/**
* @Route("/home/jobs/{id}/orders/add", name="orders_add")
*/
public function new(EntityManagerInterface $em, Request $request, UserInterface $user, Jobs $jobs)

It is still unclear to me how to pass the job id to my order form though. Maybe I'm going down the wrong path as I was trying to set it like I set the current user:
$orders->setOrdersby($this->getUser());
Because when I do
$orders->setOrdersjob($jobs->getId());
I get the error Argument 1 passed to App\Entity\Orders::setOrdersjob() must be an instance of App\Entity\Jobs or null, int given

Reply

I just spot your problem. You're working directly with the Jobs id, if you change your controller's argument $id to Jobs $job, then you'll get the Jobs object and then you can set it on the $order. It's a little bit of magic that Symfony give us for free
You can check this chapter if you wan't to know a bit more about Param Converters https://symfonycasts.com/sc...

Cheers!

Reply
Brandon Avatar

Diego, Yes that was my error, I changed $id to Jobs $jobs and I am able to render the jobname from that on my template, but when I use
$orders->setOrdersjob($jobs->getId());
I get the error Argument 1 passed to App\Entity\Orders::setOrdersjob() must be an instance of App\Entity\Jobs or null, int given
Is there another piece I am missing?

I'm so close, thank you so much for your help.

Reply
Brandon Avatar

Diego, I've got it working by using the following in my controller:
$orders->setOrdersjob($this->getDoctrine()->getRepository(Jobs::class)->findOneById($jobs));
Is that the correct Symfony way?

Reply

Hey Brandon

What you're doing works but you're doing unnecessary things. You already have the Job object, so you don't have to fetch it again from the Database. You can just do $orders->setOrdersjob($jobs);.

Second, when you want to fetch by id, you can do this $repository->find($jobs->getId());

Reply
Brandon Avatar

Diego, that is fantastic, everything is working, thank you so much!

1 Reply
Brandon Avatar

Diego, adding more to my form, I have the following:
->add('orderstoo', EntityType::class, [
'class' => EmailList::class,
'query_builder' => function (EntityRepository $er) use ($jobs) {
return $er->createQueryBuilder('e')
->where('e.emaillistjob', ':uid')
->setParameter('uid', $jobs->getId())
->orderBy('e.emailname', 'DESC');
},
'choice_label' => 'emailname',
But I get the error Undefined variable, I'm not sure how I can use the jobid in my form builder. Any suggestions?

Reply

Hey Brandon

The problem is that you are passing the jobsId instead of the jobs object. Thanks to Doctrine, your entities work with objects instead of raw id's
You may want to watch this two courses about Doctrine so you deeply understand how things work
https://symfonycasts.com/sc...
https://symfonycasts.com/sc...

Those tutorials were built on Symfony3 but the concepts of Doctrine are still relevant (Nothing critical has changed since then)

Cheers!

Reply
Akavir S. Avatar
Akavir S. Avatar Akavir S. | posted 2 years ago

Hello !
I'm trying to add some html attributes into my ChoiceType form.



->add('postalCode', ChoiceType::class,
[


'choices' => [
'1er Arrondissement'=>'69001',
'2eme Arrondissement'=>'69002',
'3eme Arrondissement'=>'69003',
'4eme Arrondissement'=>'69004',
'5eme Arrondissement'=>'69005',
'6eme Arrondissement'=>'69006',
'7eme Arrondissement'=>'69007',
'8eme Arrondissement'=>'69008',
'9eme Arrondissement'=>'69009'
],

'choice_attr' => [
'"69001"' => ['data-test' => 'test 1'],
'69002' => ['data-test' => 'test 2'],
'69003' => ['data-test' => 'test 3'],
]

But nothing changes,
How can i fix ?

Reply

Hey Virgile,

Please, try to use a callback function as shown in this example: https://symfony.com/doc/cur...

Does it helps you?

Cheers!

1 Reply
Akavir S. Avatar

Thanks you victor ! It works !

Now lets imagine that i want to add a twig filter (from the form) ? How can i do that ? What could be syntax ?

Reply

Hey Virgile,

Glad to hear it works for you!

Hm, where do you want to add a Twig filter? And what filter? :) If you want to use a separate service in your form type - you need inject that service into your form type first using dependency injection. Or you can pass the object in the array of options while creating a form.

I hope this helps!

Cheers!

Reply
Akavir S. Avatar

Hi Victor,

My filter is converting Postal Code into districts exemple: 75001 = Paris 1st.

My form field is using entity type and get back ''pure'' PostalCode From data base, i just wondering if it was possible to change it from the formType. By changing i mean apply the filter by passing it into an array of options directly form the form

I hope that i'm clear

Thanks you for your great help :)

Reply

Hey Virgile,

I think I got it. If you're using EntityType for this postal code, you can take a look at "query_builder" option, here's the example: https://symfony.com/doc/cur... - you can write any custom query you need to filter the postal code entities thanks to the Doctrine query builder.

I hope this helps!

Cheers!

Reply
Artur C. Avatar
Artur C. Avatar Artur C. | posted 3 years ago

Hi,

in my case this trick with form theme a single field just does not work :-(.
I copy the block_prefix, append _row at the and, but the field itself stil gets rendered as default form element - in my case TextType.
Any idea which aspect/configuration of my symfony 4.2 installation prevents this?

Reply

Hey @Artur

Hmm, that's odd. Can you debug a bit and find the exact name of your form field? Probably something else is wrong

Cheers!

Reply
Default user avatar

And also, the template must extend another template, otherwise the field template block gets rendered as itself additionally to beeing used in the target form row. Quite important details to mention :-)

Reply
Default user avatar

It turned out the following declaration is needed for this to work: {% form_theme form _self %}

Reply

Hey @Artur

Yes, that's needed so such template is considered as a form theme where you can override blocks or add new ones. Forms rendering system is a complex topic :)

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.6
        "symfony/console": "^4.0", // v4.1.6
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.6
        "symfony/framework-bundle": "^4.0", // v4.1.6
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.6
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.6
        "symfony/validator": "^4.0", // v4.1.6
        "symfony/web-server-bundle": "^4.0", // v4.1.6
        "symfony/yaml": "^4.0", // v4.1.6
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
        "symfony/dotenv": "^4.0", // v4.1.6
        "symfony/maker-bundle": "^1.0", // v1.8.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.6
    }
}