Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Lexical Bundle Laravel Package

flexible-ux/lexical-bundle

The Flexible UX Lexical Bundle brings the Lexical rich text editor to Laravel applications, offering ready-to-use editor components, seamless content handling, and a smooth authoring experience for admin interfaces and CMS pages alike.

View on GitHub
Deep Wiki
Context7

Getting Started

Confirm the stack first. This is a Symfony bundle, not Laravel. It only makes sense if the app already runs symfony/asset-mapper, symfony/stimulus-bundle, and symfony/ux-icons. In a Laravel/Inertia codebase it is effectively unusable — you'd be better off wiring Lexical directly via npm/Vite.

composer require flexible-ux/lexical-bundle
php bin/console importmap:require lexical @lexical/rich-text   # only if not auto-registered
php bin/console debug:asset-map | grep -i lexical              # verify bundle assets are exposed
php bin/console debug:container --tag=form.type | grep -i lexical

That last command is the fastest way to find the exact FQCN of the form type and see which options it accepts, since documentation is thin (9 stars, 0 dependents).

First use case: drop a rich-text field into an existing admin form.

use FlexibleUx\LexicalBundle\Form\LexicalEditorType; // confirm FQCN from debug:container

$builder
    ->add('body', LexicalEditorType::class, [
        'label' => 'Article body',
        'required' => false,
    ]);

Then make sure your base layout calls {{ importmap('app') }} and that the bundle's Stimulus controller is registered (Flex recipe or manual entry in assets/controllers.json). Render the form normally; the widget should mount a contenteditable surface and keep a hidden input in sync.

Where to look first: the bundle's Form/ directory (type + any data transformer), assets/ (the Stimulus controller), and templates/ (form theme block). Those three files answer 90% of "how do I change X" questions.

Implementation Patterns

Store sanitized HTML, not raw JSON, unless you have a reason. Most entities will just have a TEXT column. Persist what the editor emits, but sanitize on the way in — the bundle is an editor, not a security layer.

use Symfony\Component\HtmlSanitizer\HtmlSanitizerInterface;

public function __construct(private HtmlSanitizerInterface $sanitizer) {}

public function setBody(?string $html): self
{
    $this->body = $this->sanitizer->sanitize($html ?? '');
    return $this;
}

Render with {{ article.body|raw }} only after that sanitization step. Configure a dedicated sanitizer profile that allows the tags your toolbar can produce (p, strong, em, ul, ol, li, a, h2, h3, blockquote).

Wrap it once, reuse everywhere. Rather than repeating options in every form:

class ArticleBodyType extends AbstractType
{
    public function getParent(): string { return LexicalEditorType::class; }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'attr' => ['data-lexical-toolbar' => 'bold italic link list'],
            'help' => 'Paste from Word is cleaned on save.',
        ]);
    }
}

Normalize "empty" before validation. A NotBlank constraint on an entity field will happily pass on <p></p> or <p><br></p>. Add a Callback or a custom constraint that strips tags and &nbsp; before asserting.

Multiple editors per page. Stimulus binds per-element, so two LexicalEditorType fields in one form generally work, but keep id/name unique and avoid manually hardcoding data-controller on a wrapper — let the form theme do it.

Turbo / Live Components. The controller should re-connect() on turbo:load automatically if it's a standard Stimulus controller. If you swap form fragments via Turbo Frames, verify the hidden input is re-bound; if not, extend the bundle's controller in your own assets/controllers/ and call the parent's connect().

EasyAdmin / custom back offices. Register the type as a field:

yield Field::new('body', 'Body')->setFormType(LexicalEditorType::class);

Watch for EasyAdmin injecting its own form theme — you may need form_theme overrides in templates/bundles/EasyAdminBundle/.

Testing. Functional tests should assert on the rendered hidden input, not the contenteditable div:

$client->submitForm('Save', ['article[body]' => '<p>Hello</p>']);
$this->assertStringContainsString('<p>Hello</p>', $article->getBody());

For JS behaviour, a single Panther/Symfony BrowserKit test covering "type → hidden input updates" is enough; don't try to unit-test Lexical itself.

Gotchas and Tips

  • AssetMapper is the whole ballgame. In dev, assets stream from vendor/; in prod you must run php bin/console asset-map:compile. If the editor renders as nothing, run debug:asset-map | grep lexical and check the browser console for a failed importmap module. importmap:audit catches stale entries.
  • Importmap entries for Lexical sub-packages. Lexical is split into lexical, @lexical/rich-text, @lexical/list, @lexical/link, etc. A missing sub-package shows up as a runtime error only when you click the corresponding toolbar button — not on page load.
  • Icons need a set installed. UX Icons does not ship icons; if the toolbar shows empty buttons, run php bin/console ux:icons:import (or install a set like symfony/ux-icons's lucide/tabler). This is the single most common "looks broken" issue.
  • CSP. If you use a strict Content-Security-Policy, you'll need style-src allowances for Lexical's injected styles. It doesn't use eval, so script-src is usually fine once importmap assets are served locally.
  • No image/file upload out of the box. Lexical's image node is not wired here. If the content model needs images, plan for a separate upload endpoint and a custom node/plugin — this is where the package stops being "lightweight" and starts being work.
  • Decide HTML vs JSON on day one. If you later want server-side rendering of Lexical state, storing JSON is better; for everything else, HTML is simpler. Migrating between the two after content exists is painful.
  • Maturity risk. 9 stars and zero dependents means you are the QA team. Pin the version ("flexible-ux/lexical-bundle": "0.1.*"), read the Stimulus controller source before relying on it, and keep a thin wrapper form type so you can swap the implementation later without touching every form.
  • Extension points, in order of safety: (1) form type options via a child type, (2) form theme template override, (3) your own Stimulus controller that extends the bundle's and registers under a different name. Avoid patching vendor/.
  • Debugging checklist when the editor is dead: debug:stimulus to confirm the controller is registered → browser console for importmap 404s → debug:asset-map for the bundle path → verify importmap() and ux_icons() (if used) are in the layout head.
  • Accessibility and i18n. Toolbar labels are the bundle's responsibility; if they aren't translated, override the template block and add |trans yourself. Also verify the hidden input carries a required attribute so HTML5 validation surfaces errors before submit.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle