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!

  • 2020-05-22 Diego Aguiar

    Hey Brandon Peterson

    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!

  • 2020-05-22 Victor Bocharsky

    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!

  • 2020-05-21 Brandon Peterson

    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?

  • 2020-05-21 Justin Finkelstein

    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!

  • 2020-05-21 Victor Bocharsky

    Hey Justin,

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

    Cheers!

  • 2020-05-21 Victor Bocharsky

    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!

  • 2020-05-20 Justin Finkelstein

    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!

  • 2020-05-20 Brandon Peterson

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

  • 2020-05-20 Diego Aguiar

    Hey Brandon Peterson

    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());

  • 2020-05-20 Brandon Peterson

    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?

  • 2020-05-20 Brandon Peterson

    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?

  • 2020-05-20 Justin Finkelstein

    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?

  • 2020-05-19 Brandon Peterson

    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.

  • 2020-05-19 Diego Aguiar

    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!

  • 2020-05-19 Brandon Peterson

    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

  • 2020-05-19 Diego Aguiar

    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!

  • 2020-05-19 Brandon Peterson

    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?

  • 2020-05-19 Diego Aguiar

    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...

  • 2020-05-19 Brandon Peterson

    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?">

  • 2020-05-18 Diego Aguiar

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

  • 2020-05-18 Brandon Peterson

    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?

  • 2020-05-18 Diego Aguiar

    Hey Brandon Peterson

    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!

  • 2020-05-18 Brandon Peterson

    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.

  • 2019-12-18 Victor Bocharsky

    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!

  • 2019-12-17 Virgile Sahaguian

    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 :)

  • 2019-12-16 Victor Bocharsky

    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!

  • 2019-12-15 Virgile Sahaguian

    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 ?

  • 2019-12-10 Victor Bocharsky

    Hey Virgile,

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

    Does it helps you?

    Cheers!

  • 2019-12-09 Virgile Sahaguian

    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 ?

  • 2019-05-09 Diego Aguiar

    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 :)

  • 2019-05-08 Artur

    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 :-)

  • 2019-05-08 Artur

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

  • 2019-04-30 Diego Aguiar

    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!

  • 2019-04-27 Artur

    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?