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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeLet'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="">×</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.
I'm facing this execption: