Setting up CKEditor 5 in Symfony, whether using Asset Mapper or Webpack Encore, is not straightforward.
In this article, we will see how to achieve this with Symfony, Asset Mapper, and a small, lazily loaded Stimulus controller, so we won't load CKEditor on pages that don't need it.
Prerequisites for this article:
- Symfony, preferably an up-to-date version (currently 6.4 or 7.2)
- Asset Mapper
- Stimulus bundle
Adding CKEditor Resources
First, let's install the CKEditor JS/CSS files using Asset Mapper:
bin/console importmap:require ckeditor5
bin/console importmap:require ckeditor5/dist/ckeditor5.min.css
# If you need translations, e.g. here French translations
bin/console importmap:require ckeditor5/translations/fr.js
CKEditor Configuration
Let's move on to the JS file that will allow us to configure CKEditor—license key, translations, and loaded plugins...
In this example, we import the basic plugins and configure the associated toolbar, while loading the translations in French.
// assets/ckeditor5.js
import {
Autoformat,
Bold,
ClassicEditor,
Essentials,
Italic,
Link,
Paragraph,
} from 'ckeditor5';
// If you need to import translations, here French translations
import coreTranslations from 'ckeditor5/translations/fr.js';
import 'ckeditor5/dist/ckeditor5.min.css';
export default class EnhancedEditor extends ClassicEditor {}
EnhancedEditor.builtinPlugins = [Autoformat, Bold, Essentials, Italic, Link, Paragraph];
EnhancedEditor.defaultConfig = {
licenseKey: 'GPL',
toolbar: [
'bold',
'italic',
'|',
'link',
'|',
'undo',
'redo',
],
// You can remove the following part if you don't need other translations
translations: [coreTranslations],
};
Stimulus controller
Here is the Stimulus controller, which allows us to load resources on demand (the controller itself being loaded lazily), and to instantiate the editors if they appear dynamically on the page.
// assets/controllers/ckeditor5_controller.js
import { Controller } from '@hotwired/stimulus';
import EnhancedEditor from '../ckeditor5.js';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
connect() {
EnhancedEditor.create(this.element).catch(error => console.error(error));
}
}
Dedicated Form Type
To make things easier, we'll create a dedicated form type that will allow us to easily load CKEditor into a form.
<?php
declare(strict_types=1);
namespace App\Form\Type;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\UX\StimulusBundle\Helper\StimulusHelper;
/**
* @extends AbstractType<string>
*/
final class CKEditor5Type extends AbstractType
{
public function __construct(
#[Autowire(service: 'stimulus.helper')]
private readonly StimulusHelper $stimulusHelper,
) {}
public function finishView(FormView $view, FormInterface $form, array $options): void
{
parent::finishView($view, $form, $options);
$attr = $this->stimulusHelper->createStimulusAttributes();
$attr->addController('ckeditor5');
$view->vars['attr'] = $attr->toArray();
}
public function getParent(): string
{
return TextareaType::class;
}
}
Finally, we can use it!
Yes, we'll finally be able to use it however we want in our forms! Here's how:
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('content', CKEditor5Type::class);
}
Et voilà! :) If you have any questions, comments, suggestions... Feel free to post a comment below :)
Comments
Posted by Stof on 21/01/2025 at 15:25.
To avoid leaking resources, your Stimulus component should also handle the case where it is remove dynamically from the page, not just when it is added.For that, you need to store the editor instance (returned asynchronously from `ClassicEditor.create()`) and call its `destroy()` method in `disconnect`