If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeThe 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
.
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.
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.
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!
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']```
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/screencast/symfony-form-theming
In your case, it looks like you would need to override the checkbox_widget
block - https://github.com/symfony/symfony/blob/6d521d40721104a684565fe6d1ca2bb0372127e/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig#L178 - 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!
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>`
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!
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?
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!
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!
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!
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?
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!
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.
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/current/form/form_themes.html#symfony-built-in-form-themes
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/current/form/form_customization.html
Or, you can watch our tutorial about Forms Rendering. It's based on Symfony3 but the main concepts are still relevant https://symfonycasts.com/screencast/symfony-form-theming
Cheers!
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?
I believe copying the entire <tr> element is the right way to do it but you can give it a try :)
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')) }}>
{{ form_widget(ordersForm.orderitems.vars.prototype.description) }}
</div>
</div>
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?">
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...
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?
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!
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
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/screencast/symfony3-doctrine-relations/param-conversion
Cheers!
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.
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?
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());
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?
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!
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 ?
Hey Virgile,
Please, try to use a callback function as shown in this example: https://symfony.com/doc/cur...
Does it helps you?
Cheers!
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 ?
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!
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 :)
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!
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?
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!
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 :-)
It turned out the following declaration is needed for this to work: {% form_theme form _self %}
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 :)
// 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
}
}
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.