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 SubscribeWe just created a Stimulus controller. Now we need to apply this controller to the "row" that's around each field. Let me make things a bit smaller. So we're going to apply the controller to this row. The code in the controller will watch the textarea for changes and render a preview.
The whole flow looks like this. When that row first appears on the page, the initialize()
method will add a preview div. Then, whenever we type into the field, Stimulus will call render()
... which will render the HTML preview. We're not going to talk more about the Stimulus code, but if you have any questions, let us know in the comments.
Thanks to the fact that admin.js
is importing bootstrap.js
, which initializes all of the controllers in the controllers/
directory, our new snarkdown_controller
is already available in the admin section. So, we can get to work!
On the field, call setFormTypeOptions()
and pass this an array. We need to set a few attributes. The first is row_attr
: the attributes that you want to add to the form "row". This is not an Easy Admin thing... it's a normal option inside Symfony's form system. Add a data-controller
attribute set to snarkdown
. I did just typo that, which is going to totally confuse future me.
Next pass an attr
option: the attributes that should be added the textarea itself. Add one called data-snarkdown-target
set to input
. In Stimulus language, this makes the textarea a "target"... so that it's easy for us to find. Also add data-action
set to snarkdown#render
.
... lines 1 - 14 | |
class QuestionCrudController extends AbstractCrudController | |
{ | |
... lines 17 - 30 | |
public function configureFields(string $pageName): iterable | |
{ | |
... lines 33 - 43 | |
yield TextareaField::new('question') | |
->hideOnIndex() | |
->setFormTypeOptions([ | |
'row_attr' => [ | |
'data-controller' => 'snarkdown', | |
], | |
'attr' => [ | |
'data-snarkdown-target' => 'input', | |
'data-action' => 'snarkdown#render', | |
], | |
]); | |
... lines 55 - 74 | |
} | |
} |
This says: whenever the textarea changes, call the render()
method on our snarkdown
controller.
Let's try this! Move over and refresh... and type a little... hmm. No preview. And no errors in the console either. Debugging time! Inspect the element. Bah! A typo on the controller name... so the controller was never initialized.
Fix that - snarkdown
- and now when we refresh, there it is! It starts with a preview... and when we type... it instantly updates to show that as bold. Awesome!
Though, we could style this a bit better... and fortunately we know how to add CSS to our admin area. In admin.css
, add a .markdown-preview
selector. This is the class that the preview div has when we add it. Let's give this some margin, a border and some padding.
... lines 1 - 6 | |
.markdown-preview { | |
margin-top: 10px; | |
border: 2px dashed #da3735; | |
padding: 5px; | |
} |
And now... neato! And to make this even cooler, in QuestionCrudController
, on the field, call ->setHelp('Preview')
.
... lines 1 - 14 | |
class QuestionCrudController extends AbstractCrudController | |
{ | |
... lines 17 - 30 | |
public function configureFields(string $pageName): iterable | |
{ | |
... lines 33 - 43 | |
yield TextareaField::new('question') | |
... lines 45 - 54 | |
->setHelp('Preview:'); | |
... lines 56 - 75 | |
} | |
} |
Help messages render below the field... so... ah. This gives the preview a little header.
So with the combination of Stimulus and an admin.js
file that imports bootstrap.js
, we can add custom JavaScript to our admin section simply by dropping a new controller into the controllers/
directory.
This does create one small problem. Every file in the controllers/
directory is also registered and packaged into the built app.js
file for the frontend. This means that users that visit our frontend are downloading snarkdown_controller
and snarkdown itself. That's probably not a security problem... but it is wasteful and will slow down the frontend experience.
My favorite way to fix this is to go into the controller and add a superpower that's special to Stimulus inside of Symfony. Put a comment directly above the controller with stimulusFetch
colon then inside single quotes lazy
.
... lines 1 - 4 | |
/* stimulusFetch: 'lazy' */ | |
export default class extends Controller { | |
... lines 7 - 26 | |
} |
What does that do? It tells Encore to not download this controller code - or anything it imports - until the moment that an element appears on the page that matches this controller. In other words, the code won't be downloaded immediately. But then, the moment a data-controller="snarkdown"
element appears on the page, it'll be downloaded via Ajax and executed. Pretty perfect for admin stuff.
Check it out. On your browser, go back to the admin section. Pull up your network tools and go to the Questions section. I'll make the tools bigger... then go edit a question. On the network tools filter, click "JS".
Check out this last entry: assets_controllers_snarkdown_controller_js.js
. That is the file that contains our snarkdown_controller
code. And notice the "initiator" is "load_script". That's a Webpack function that tells me that this was downloaded after the page was loaded. Specifically, once the textarea appeared on the page.
And if we visit any different page... yep! That file was not downloaded at all because there is no data-controller="snarkdown"
element on the page.
Next, it's finally time to do something with our dashboard! Let's render a chart and talk about what other things you can do with your admin section's landing page.
Hey Jony,
Thanks for this hint! Yeah, you should have this to make it work... we did it in previous chapters: https://symfonycasts.com/screencast/easyadminbundle/assets#codeblock-9cb529831f - so if you follow this course from the beginning - you should have this.
Cheers!
Yes, Victor.
In my case was that I called the CSS with another name than admin
so: If you follow the tutorial there is no problem when admin.js include the admin.css due to the name being the same in the ->addWebpackEncoreEntry('admin');
but in my case, I called adminstyles
so I had ->addWebpackEncoreEntry('adminstyles');
.
When I created an admin.js
that includes adminstyles.css
in the tutorial we updated the webpack.config.js
but no reference to taking care of the configureAsset()
method of DashboardController, obviously because it is the same name.
So the advice here is: "if you are taking a look at a tutorial, follow the tutorial! " XDD.
Hey Jony,
Ah, I see! Yes... that's a good practice to name the JS and CSS files with the same exact name :) I think it's enough, because you have the file extension anyway, so it's clear that admin.css
file is for "styles" for admin.js
:)
Cheers!
Hello, I have a question if it's possible setting a data-controller attribute to a panel in stead of a field. To create a controller surrounding multiple forms at the same time. I'm not able to get this working.
The following code works to add an extra class value to the panel:
yield FormField::addPanel('User Details')->addCssClass('foobar');
But the following code doesn't do anything:
yield FormField::addPanel('User Details')->setFormTypeOptions([
'row_attr' => [
'data-controller' => 'foo',
]
]);
The goal is to group a couple of fields and create a single controller. Then adding the fields as a target.
Best regards
Hey Coding010,
Yeah, setFormTypeOptions()
won't work for that addPanel()
because in the source code EA just does not render any additional things like this. There's another method called setCssClass()
but it will help to set a CSS class to that whole panel wrapper and that's it. But if you want to add some data attributes - I think you should override a template for this, in specific vendor/easycorp/easyadmin-bundle/src/Resources/views/crud/form_theme.html.twig
one - you will find a <div class="form-panel">
tag there, add your data controller there.
I hope this helps!
Cheers!
Hi Victor, thanks for your reply.
I browsed around a bit on EasyAdmin's Github page and found this newly added feature:
https://github.com/EasyCorp/EasyAdminBundle/pull/5488.
It was added in Easy Admin 4.4.3.
This helped me out, as now I can set a Stimulus controller data attribute on the <body>
element of a crud.
It's not specifically around a group of field as I first wanted, but this should do the trick.
Here a quick code example:
Override the templates via the CrudController.
// src/Controller/Admin/Crud/FooCrudController.php
public function configureCrud(Crud $crud): Crud
{
return $crud
->overrideTemplates([
'crud/new' => 'admin/crud/new.html.twig',
'crud/edit' => 'admin/crud/edit.html.twig',
])
;
}
Note that 'crud/layout'
doesn't work, even though the examples on the Symfony documentation page say it should.
And then create the template to override:
{# templates/admin/crud/new.html.twig #}
{% extends '@EasyAdmin/crud/new.html.twig' %}
{% block body_attr %}
{{ stimulus_controller('admin/foo') }}
{% endblock body_attr %}
This adds the data-controller
attribute to the body
.
<body data-controller="admin--foo">
...
Hope this helps out others with this "problem" as well.
Hello! If I start a stimulus application, this way
public function configureAssets(Assets $assets): Assets
{
return parent::configureAssets($assets)->addWebpackEncoreEntry('app');
}
or this way
{{ encore_entry_script_tags('admin') }}
then actions menu(3 dorts) stops working. It doesn't expand if i click it.
I know it is out of scope, but what if we do not want admin related (stimulus) controllers to load at all on non-admin pages? Let's say someone manipulate the DOM.
Hey julien_bonnier
Great question. I had the same when developed some internal stuff. And there is pretty easy question. IIRC Stimulus controller loader is configured in assets/bootstrap.js
so you can duplicate it and configure to load a separate folder for admin, so you will have everything separated.
Cheers!
// composer.json
{
"require": {
"php": ">=8.1.0",
"ext-ctype": "*",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/doctrine-bundle": "^2.1", // 2.5.5
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.2.1
"doctrine/orm": "^2.7", // 2.10.4
"easycorp/easyadmin-bundle": "^4.0", // v4.0.2
"handcraftedinthealps/goodby-csv": "^1.4", // 1.4.0
"knplabs/knp-markdown-bundle": "dev-symfony6", // dev-symfony6
"knplabs/knp-time-bundle": "^1.11", // 1.17.0
"sensio/framework-extra-bundle": "^6.0", // v6.2.5
"stof/doctrine-extensions-bundle": "^1.4", // v1.7.0
"symfony/asset": "6.0.*", // v6.0.1
"symfony/console": "6.0.*", // v6.0.2
"symfony/dotenv": "6.0.*", // v6.0.2
"symfony/flex": "^2.0.0", // v2.0.1
"symfony/framework-bundle": "6.0.*", // v6.0.2
"symfony/mime": "6.0.*", // v6.0.2
"symfony/monolog-bundle": "^3.0", // v3.7.1
"symfony/runtime": "6.0.*", // v6.0.0
"symfony/security-bundle": "6.0.*", // v6.0.2
"symfony/stopwatch": "6.0.*", // v6.0.0
"symfony/twig-bundle": "6.0.*", // v6.0.1
"symfony/ux-chartjs": "^2.0", // v2.0.1
"symfony/webpack-encore-bundle": "^1.7", // v1.13.2
"symfony/yaml": "6.0.*", // v6.0.2
"twig/extra-bundle": "^2.12|^3.0", // v3.3.7
"twig/twig": "^2.12|^3.0" // v3.3.7
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.1
"symfony/debug-bundle": "6.0.*", // v6.0.2
"symfony/maker-bundle": "^1.15", // v1.36.4
"symfony/var-dumper": "6.0.*", // v6.0.2
"symfony/web-profiler-bundle": "6.0.*", // v6.0.2
"zenstruck/foundry": "^1.1" // v1.16.0
}
}
If does not work for anyone under symfony6 add to the DashboardController