Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Form Theming For a Completely Custom 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 $8.00

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

Login Subscribe

Let's look at one more way to make a ridiculously custom field. Right now, we're using the CollectionType... which works... but is totally ugly. And the only reason it works is that we did a lot of work in a previous tutorial to get the relationship setup properly.

And even if you can get the CollectionType working, you may want to add more bells and whistles to the interface. So here's the plan: we're not going to use the CollectionType... at all. Instead, we'll write our own HTML and JavaScript to create our own widget, which will use AJAX to delete and add new entries. Actually, we won't do all of that right now - but I'll show you how to get things setup so you can get back to writing that custom code.

Configuring a Fake Field

Back in config.yml, find the genusScientists field, change its type to text and delete the 4 options:

... lines 1 - 80
easy_admin:
... lines 82 - 94
entities:
Genus:
... lines 97 - 117
form:
fields:
... lines 120 - 128
-
property: 'genusScientists'
type: 'collection'
type_options:
entry_type: AppBundle\Form\GenusScientistEmbeddedForm
allow_delete: true
allow_add: true
by_reference: false
... lines 137 - 148

Whaaaat? Won't this break like crazy!? The genusScientists field holds a collection of GenusScientist objects... not just some text!

Totally! Except that we're going to add one magic config: mapped: false:

... lines 1 - 80
easy_admin:
... lines 82 - 97
entities:
Genus:
... lines 100 - 120
form:
fields:
... lines 123 - 131
-
... lines 133 - 134
type_options:
mapped: false
... lines 137 - 149

As soon as I do that, this is no longer a real field. I mean, when the form renders, it will not call getGenusScientists(). And when we submit, it will not call setGenusScientists(). In fact, you could even change the field name to something totally fake... and it would work fine! This field will live in the form... but it doesn't read or set data on your entity. It's simply a way for us to "sneak" a fake field into our form.

Like we did in the last chapter, add a CSS class to the field: js-scientists-field:

... lines 1 - 80
easy_admin:
... lines 82 - 97
entities:
Genus:
... lines 100 - 120
form:
fields:
... lines 123 - 131
-
... lines 133 - 134
type_options:
mapped: false
attr: { class: 'js-genus-scientists-field' }
... lines 138 - 149

This time I'll use the standard attr option under type_options... but not for any particular reason.

Let's go see what this looks like! Yep, it's just a normal, empty text field: empty because it's not bound to our entity... at all - so it has no data.

Form Theme for One Field

Here's the goal: I want to replace this text field with our own Twig code, where we can build whatever crazy genus scientist-managing widget we want! How? The answer is to work with the form system: create a custom form theme that just overrides the widget for this one field.

To find out how, click the clipboard icon to get into the form profiler. Under genusScientists, open up the view variables. See this one called block_prefixes? This is the key for knowing the name of the block you should create in a form theme to customize this field. For example, to customize the widget for this field, we could create a block called form_widget, text_widget or _genus_genusScientists_widget. The last block would only affect this one field.

Copy that name. Then, in app/Resources/views/easy_admin, create a new file called _form_theme.html.twig. Add the block: _genus_genusScientists_widget with its endblock:

{% block _genus_genusScientists_widget %}
Are you feeling powerful?
{% endblock %}

Are you feeling powerful yet? If not, you will soon. Before we start writing our awesome code, we need to tell Symfony to use this form theme. In previous tutorials, we learned how to add a custom form theme to our entire app... but in this case, we really only need this inside of our easy admin area.

EasyAdminBundle gives us a way to do this. In config.yml, under design, add form_theme. We're actually going to add two: horizontal and easy_admin/_form_theme.html.twig:

... lines 1 - 80
easy_admin:
... line 82
design:
... lines 84 - 91
form_theme:
- horizontal
- easy_admin/_form_theme.html.twig
... lines 95 - 149

EasyAdminBundle actually ships with two custom form themes: horizontal and vertical... the difference is just whether the labels are next to, or above the fields. By default, horizontal is used. When you add your own custom form theme, you need to include horizontal or vertical... to keep using it.

Ok... let's kick the tires! Close the profiler and refresh. Ahhhhhhh!

Unrecognized option "form_themes" under "easy_admin.design"

Ok, my bad. It's form_theme:

... lines 1 - 80
easy_admin:
... line 82
design:
... lines 84 - 91
form_theme:
... lines 93 - 149

Thank you validation.

Now... we've got it! Our text shows up where the field should be. We can put anything here: like some HTML or an empty div that JavaScript fills in. Heck, we could create a React or Vue.js app and point it at this div. It's simple... but the possibilities are endless.

Rendering the Genus Scientists

Let's see a quick example to get the creative juices flowing! Let's create a table that lists all of the genus scientists:

{% block _genus_genusScientists_widget %}
<table class="table">
<tbody>
... lines 4 - 12
</tbody>
</table>
{% endblock %}

Inside a tbody, we're ready to loop over the scientists! But... uh... how can I get them? What variables do I have access to right here?

Go back to the form profiler, find genusScientists and look again at the view variables. These are all the variables that we have access to from within our form theme. But because we set the field to mapped false... um... we actually don't have access to our Genus object! That's a problem. But! Because we're inside EasyAdminBundle, it gives us a special easyadmin variable... with an item key equal to our Genus! Phew!

Ok! In the table, loop: for genusScientist in easyadmin.item.genusScientists:

{% block _genus_genusScientists_widget %}
<table class="table">
<tbody>
{% for genusScientist in easyadmin.item.genusScientists %}
... lines 5 - 11
{% endfor %}
</tbody>
</table>
{% endblock %}

Add the tr and print out a few fields: genusScientist.user and genusScientist.yearsStudied:

{% block _genus_genusScientists_widget %}
<table class="table">
<tbody>
{% for genusScientist in easyadmin.item.genusScientists %}
<tr>
<td>{{ genusScientist.user }}</td>
<td>{{ genusScientist.yearsStudied }} years</td>
... lines 8 - 10
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

Let's also add a fake delete link with a class and a data-url attribute. But leave it blank:

{% block _genus_genusScientists_widget %}
<table class="table">
<tbody>
{% for genusScientist in easyadmin.item.genusScientists %}
<tr>
<td>{{ genusScientist.user }}</td>
<td>{{ genusScientist.yearsStudied }} years</td>
<td>
<a href="#" class="js-delete-scientist" data-url="">&times;</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

In your app, you might create a delete AJAX endpoint and use the path() function to put that URL here so you can read it in JavaScript.

Cool! To make this a bit more realistic, open custom_backend.js. Let's find those .js-delete-scientist elements and, on click, call a function. Add the normal e.preventDefault() and... an alert('to do'):

$(document).ready(function () {
... lines 2 - 13
$('.js-delete-scientist').on('click', function(e) {
e.preventDefault();
alert('todo');
});
});

The rest, is homework!

Let's try it! There it us! A nice table with a delete icon. There's more work to do, but you can totally do it! This is just normal coding: create a delete endpoint, call it via JavaScript and celebrate!

With form stuff behind us, let's turn to adding custom actions, like, a publish button.

Leave a comment!

6
Login or Register to join the conversation

I'm facing this execption:

An exception has been thrown during the rendering of a template ("No block "form" found while rendering the form.").
Reply

Hey Ahmedbhs,

Check your template. Something is wrong with your syntax, search for "form" keyword there and try to double check the syntax

Cheers!

Reply
Default user avatar
Default user avatar Common Pike | posted 3 years ago

The form_theme part seems to be a bit out of date. Check https://symfony.com/doc/mas...

Reply

I am going to put this here, maybe others will find it useful.
As of Symfony 4.3 form theme has also changed (see: https://symfony.com/blog/new-in-symfony-4-3-simpler-form-theming)

You can use this 'block_prefix' param in easyadmin under type_options:

`
...

  • property: 'genusScientists'
    type_options:
    ...
    block_prefix: 'genus_scientists'
    `

then in _form_theme.html.twig:
<br />{%block genus_scientists_widget %}<br /> ...<br />{%endblock %}<br />

Reply

Hey edin

Thanks for sharing it. EasyAdmin has changed a lot since this tutorial but many of the topics taught here are still relevant

Cheers!

1 Reply

Hey Common Pike!

Yea, this part is out of date. The tutorial uses version 1 of EasyAdminBundle - and the form theming default value changed in version 2. Also, version 3 is currently being worked on but hasn't been released yet - that's the version of the docs you pointed to - I'm not sure if there are any significant changes or not yet - but watch out for the version checker on top :).

Is there anything that you're trying to do that's not working well with the updated form theme stuff? Let us know!

Cheers!

Reply
Cat in space

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

This tutorial is built on an older version of Symfony & EasyAdminBundle. Many of the concepts are the same, but you can expect major differences in newer versions.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.3.*", // v3.3.18
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.10.3
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.5
        "symfony/swiftmailer-bundle": "^2.3", // v2.6.7
        "symfony/monolog-bundle": "^2.8", // v2.12.1
        "symfony/polyfill-apcu": "^1.0", // v1.17.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
        "knplabs/knp-markdown-bundle": "^1.4", // 1.7.1
        "doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
        "stof/doctrine-extensions-bundle": "^1.2", // v1.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "javiereguiluz/easyadmin-bundle": "^1.16" // v1.17.21
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.1.7
        "symfony/phpunit-bridge": "^3.0", // v3.4.40
        "nelmio/alice": "^2.1", // v2.3.5
        "doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
    }
}
userVoice