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!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.0.2",
        "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
    }
}