Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Custom Field JavaScript

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

Go to the Question edit page. Ok: the question itself is in a textarea, which is nice. But it would be even better if we could have a fancy editor that helps with our markup.

Hello TextEditorField

Fortunately EasyAdmin has something just for this. In QuestionCrudController, for the question field, instead of a textarea, change to TextEditorField.

... lines 1 - 13
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
... line 15
class QuestionCrudController extends AbstractCrudController
{
... lines 18 - 31
public function configureFields(string $pageName): iterable
{
... lines 34 - 44
yield TextEditorField::new('question')
... lines 46 - 66
}
}

Refresh the page and... we have a cute lil' editor for free! Nice!

If you look inside of TextEditorField... you can see a bit about how this works. Most importantly, it calls addCssFiles() and addJsFiles(). Easy Admin comes with extra JavaScript and CSS that adds this special editor functionality. And by leveraging these two methods, that CSS and Javascript is included on the page whenever this field is rendered.

Adding JavaScript to our Admin Encore Entry

So this is nice... except that... our question field isn't meant to hold HTML. It's meant to hold markdown... so this editor doesn't make a lot of sense.

Let's go back to using the TextareaField.

... lines 1 - 30
public function configureFields(string $pageName): iterable
{
... lines 33 - 43
yield TextareaField::new('question')
... lines 45 - 65
}
... lines 67 - 68

So we don't need a fancy field... but it would be really cool if, as we type inside of here, a preview of the final HTML were rendered right below this.

Let's do that! For this to happen, we're going to write some custom JavaScript that will render the markdown. We could also make an Ajax call to render the markdown... it doesn't matter. Either way, we need to write custom JavaScript!

Open up the webpack.config.js file. We do have a custom admin CSS file. Now we're also going to need a custom admin.js file. So up in the assets/ directory, right next to the main app.js that's included on the frontend, create a new admin.js file.

Inside, we're going to import two things. First, import ./styles/admin.css to bring in our admin styles. And second, import ./bootstrap.

3 lines assets/admin.js
import './styles/admin.css';
import './bootstrap';

This file is also imported by app.js. Its purpose is to start the Stimulus application and load anything in our controllers/ directory as a Stimulus controller.

If you haven't used Stimulus before, it's not required to do custom JavaScript... it's just the way that I like to write custom JavaScript... and I think it's awesome. We have a big tutorial all about it if you want to jump in.

So the admin.js file imports the CSS file and it also initializes the Stimulus controllers. Now over in webpack.config.js, we can change this to be a normal entrypoint... and point it at ./assets/admin.js.

... lines 1 - 8
Encore
... lines 10 - 23
.addEntry('admin', './assets/admin.js')
... lines 25 - 73
;
... lines 75 - 77

The end result is that Encore will now output a built admin.js file and a built admin.css file... since we're import CSS from our JavaScript.

And because we just made a change to the Webpack config file, find the terminal tab that's running Encore, stop it with "control+C" and restart it:

yarn watch

Perfect! It says that the "admin" entrypoint is outputting an admin.css file and an admin.js file. It also splits some of the code into a few other files for performance.

Thanks to this change, if you go refresh any page... and view the page source, yup! We still have a link tag for admin.css but now the admin JavaScript is also being included, which is all of this stuff right here. We now have the ability to add custom JavaScript.

The Stimulus Controller

So here's the plan. We're going to install a JavaScript markdown parser called snarkdown. Then, as we type into this box, in real time, we'll use it to render an HTML preview below this. And to hook all of this up, we're going to write a Stimulus controller.

Let's start by installing that library. Over in the main terminal tab, run:

yarn add snarkdown --dev

Excellent! Next, up in assets/controllers/, create a new file called snarkdown_controller.js. And because this tutorial is not a Stimulus tutorial, I'll paste in some contents.

import { Controller } from '@hotwired/stimulus';
import snarkdown from 'snarkdown';
const document = window.document;
export default class extends Controller {
static targets = ['input'];
outputElement = null;
initialize() {
this.outputElement = document.createElement('div');
this.outputElement.className = 'markdown-preview';
this.outputElement.textContent = 'MARKDOWN WILL BE RENDERED HERE';
this.element.append(this.outputElement);
}
connect() {
this.render();
}
render() {
const markdownContent = this.inputTarget.value;
this.outputElement.innerHTML = snarkdown(markdownContent);
}
}

What's inside of here... isn't that important. But to get it to work, we're going to need some custom attributes that will attach this controller to the form field. Let's do that next and use a performance trick so that our new controller isn't unnecessarily downloaded by frontend users.

Leave a comment!

8
Login or Register to join the conversation
mofogasy Avatar
mofogasy Avatar mofogasy | posted 1 year ago | edited

Hi, does anyone knows why I cannot get bootstrap features to work?
Things like <a href="https://getbootstrap.com/docs/4.0/components/carousel/&quot;&gt; carousel</a> (no sliding) <a href="https://getbootstrap.com/docs/4.0/components/dropdowns/&quot;&gt;dropdown&lt;/a&gt; (no dropdown)
don't work in the base.html.twig ?

isn't the {{ encore_entry_script_tags('app') }} in that file supposed to add bootstrapt link since app.js already import bootstrap ?

Reply

Hey grenouille!

> isn't the {{ encore_entry_script_tags('app') }} in that file supposed to add bootstrapt link since app.js already import bootstrap ?

In theory, yes. But EasyAdmin also has its own Bootstrap JavaScript. What do you want to accomplish? And what does your app.js look like exactly?

Cheers!

Reply
Colin-P Avatar
Colin-P Avatar Colin-P | posted 1 year ago

Hello,
I noticed an issue that occurs when my javascript assets require bootstrap. For example, if I import 'bootstrap' (Twitter Bootstrap, not the stimulus bootstrap file) in 'admin.js', then the dropdown actions (such as the avatar in the top right, or the 3 dots at the end of the row on the index page) stop working.

I made sure that my project required the exact same version of Bootstrap as EasyAdmin. Could anyone shine some light onto what the issue could be?

thanks,
Colin

Reply

Hey Colin,

I can't say for sure... but I would like to know why do you include bootstrap to your admin? Well, first of all, EasyAdmin already has bootstrap included, it's based on the latest bootstrap. So, you probably don't want to include your own one, even if you're including the same version, because it will cause some conflicts. Well, maybe not conflicts, but duplications for sure, as both CSS files will apply same rules twice.

But, if you curious to know the reason it breaks sometimes - then, in theory, it's because that EasyAdmin is based on bootstrap but also slightly modifies it. And so, the workflow when you include your own bootstrap is the next probably:

1. EasyAdmin includes bootstrap
2. EasyAdmin slightly modifies bootstrap for its needs
3. You include the bootstrap again, that overrides some rules that were overwritten by EasyAdmin

Well, sometimes like this, I can't think of anything else. Or, it might be so that you're including a bit different version of bootstrap that causes those issues. But in theory, if EA already has bootstrap (and it does have it) - you better don't include it again and just reuse the bootstrap EasyAdmin uses behind the scene.

I hope this helps.

Cheers!

Reply

Just for future reference, EA includes a global bootstrap var that can be used: https://github.com/EasyCorp...

There is no need to import bootstrap

1 Reply

Hey Edin,

Nice, thanks for sharing this!

Cheers!

Reply
Colin-P Avatar
Colin-P Avatar Colin-P | Victor | posted 1 year ago | edited

Hey Victor,

I agree, there isn't a reason to include bootstrap again. I just noticed that the dropdowns stopped working after I showed another developer how to include custom webpack entries, and narrowed the cause of the error to importing bootstrap in our custom javascript we were adding to EasyAdmin.

For instance in our custom javascript we had:


import { Modal } from 'bootstrap';

which caused the error.

Probably from the beginning this should have been:


import { Modal } from 'bootstrap/js/dist/modal';

which works fine.

The theory of the EasyAdmin bootstrap modifications makes sense, I didn't think to check that out, so thank you for pointing that out.

Thanks for the help,
Colin

Reply

Hey Colin,

Ah, I see! So, it's good you thought about why it happened 👍. Yeah, not sure about how to import that Modal correctly, probably Bootstrap docs should cover, just look closer to the version of Bootstrap, they have a bit different docs for different version of it. But if it works with "bootstrap/js/dist/modal" path - great, thanks for sharing it!

Yeah, EasyAdmin have some tweaks to the bootstrap defaults... just to look less "bootstrappy" and be more unique, you know.

Anyway, I hope it helped!

Cheers!

Reply
Cat in space

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

What PHP libraries does this tutorial use?

// 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
    }
}
userVoice