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

Filament Monaco Editor Laravel Package

abdelhamiderrahmouni/filament-monaco-editor

A Filament form field that brings the Monaco Editor, the engine behind VS Code, into your admin panels. Get syntax highlighting, autocompletion, and a full-featured code editing experience for JSON, PHP, JavaScript, and more, right inside your Filament forms.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up

composer require abdelhamiderrahmouni/filament-monaco-editor

The package is a single Filament form field, so there is usually no plugin registration step—no ->plugin() call in your panel provider. If the release you install ships publishable assets/views, run the tagged publish commands and then php artisan filament:assets (Filament v4) so the compiled JS/CSS lands in public/.

Check the exact PHP namespace before you start typing imports—confirm it in the source rather than trusting memory:

grep -r "class MonacoEditor" vendor/abdelhamiderrahmouni/filament-monaco-editor/src

Then drop the field into any Filament form:

use AbdelhamidErrahmouni\FilamentMonacoEditor\MonacoEditor; // verify against vendor/src

MonacoEditor::make('content')
    ->language('javascript')
    ->height('400px')
    ->required();

Where to look first

  1. vendor/abdelhamiderrahmouni/filament-monaco-editor/src/MonacoEditor.php — the entire public API lives in this one class. It is short; read it end-to-end before designing around it.
  2. The package's Blade view (resources/views/) — this is where Monaco is bootstrapped via Alpine/x-init and how it syncs with Livewire. Understanding this file answers 90% of "why isn't my state updating" questions.
  3. composer.json — check the filament/filament constraint to confirm v3 vs v4 compatibility for the version you're pulling.

First real use case

The highest-value entry point is a Settings page or a resource field holding a code-shaped string: a JSON config blob, a CSS override block, an email template, a regex, or a Blade/JS snippet. You get syntax highlighting, bracket matching, and find/replace for free, which is a large usability jump over a Textarea for anything over ~20 lines.

// app/Filament/Pages/ManageBranding.php
protected function getFormSchema(): array
{
    return [
        MonacoEditor::make('custom_css')
            ->language('css')
            ->height('500px')
            ->helperText('Appended to the panel stylesheet at boot.'),
    ];
}

Save, reload, and confirm the value round-trips through Livewire before building anything more elaborate on top of it.

Implementation Patterns

Treat ->options() as your escape hatch

Most of Monaco's richness is not wrapped in dedicated PHP methods. Pass the native Monaco editor options straight through:

MonacoEditor::make('script')
    ->language('php')
    ->height('600px')
    ->options([
        'minimap' => ['enabled' => false],
        'wordWrap' => 'on',
        'fontSize' => 13,
        'tabSize' => 4,
        'lineNumbers' => 'on',
        'scrollBeyondLastLine' => false,
        'automaticLayout' => true,
        'renderWhitespace' => 'selection',
    ]);

automaticLayout => true is worth setting by default—it saves you from manual editor.layout() calls when the container resizes (tabs, modals, collapsible sections).

Dynamic language from a sibling field

A common pattern for "snippet" models is a language select plus a code editor that re-highlights when the language changes:

Forms\Components\Select::make('language')
    ->options([
        'php' => 'PHP',
        'javascript' => 'JavaScript',
        'json' => 'JSON',
        'blade' => 'Blade',
    ])
    ->live()
    ->required(),

MonacoEditor::make('body')
    ->language(fn (Get $get) => $get('language') ?? 'plaintext')
    ->key(fn (Get $get) => 'editor-'.$get('language')) // force remount on change
    ->columnSpanFull(),

Monaco's language cannot be reliably hot-swapped from PHP alone; forcing a remount with ->key() (or a Livewire wire:key in your own view) is the pragmatic route. Make sure the editor's current value is already synced before the remount, or you will silently drop keystrokes.

Validation and normalisation on save

Monaco gives you a string. Treat it as untrusted input and normalise before persisting:

MonacoEditor::make('settings_json')
    ->language('json')
    ->rules(['json'])
    ->dehydrateStateUsing(function (?string $state) {
        if (blank($state)) {
            return null;
        }

        // Re-encode so stored JSON is always canonical
        return json_encode(
            json_decode($state, associative: true, flags: JSON_THROW_ON_ERROR),
            JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
        );
    });

Laravel's json validation rule is the cheapest guard against a user leaving the buffer with a trailing comma. Surface Monaco's own squiggles as a hint, but never rely on client-side diagnostics for correctness.

Read-only viewers

For an infolist-style "view the raw payload" panel, disable editing but keep highlighting and folding:

MonacoEditor::make('payload')
    ->language('json')
    ->height('350px')
    ->options(['readOnly' => true, 'domReadOnly' => true, 'minimap' => ['enabled' => false]]);

If the field is purely presentational, add ->dehydrated(false) so a read-only editor can never be smuggled into the save payload.

Inside repeaters, builders, and relation managers

Each repeater item gets its own editor instance. Two things matter:

  • Give the field a stable, unique identity so Livewire does not recycle the wrong editor DOM node—->key() or a wire:key in a custom view.
  • Beware of editors mounted inside collapsed repeater items. A container with height: 0 produces a blank editor; when the item is expanded you may need to trigger a layout, either via automaticLayout or by re-initialising the component.

For heavy forms, constrain the number of simultaneous editors. Ten Monaco instances on one page is a noticeable jank source; a single editor in a modal opened from the repeater item is usually the better interaction.

Custom themes and editor behaviour via extra JS

If you need a bespoke theme or command registration (a "Format document" button, a custom autocomplete provider), the cleanest path is to monaco.editor.defineTheme() in your panel's custom JS before the field initialises, then pass the theme name:

// resources/js/admin.js, registered via ->viteTheme() or Filament's asset API
monaco.editor.defineTheme('brand-dark', { base: 'vs-dark', inherit: true, rules: [], colors: {} });
MonacoEditor::make('query')
    ->language('sql')
    ->options(['theme' => 'brand-dark']);

If you go this route, you are effectively self-hosting Monaco's loader—see the CDN gotcha below.

Gotchas and Tips

Monaco almost certainly loads from a CDN by default. This is the single most common production surprise. It breaks on air-gapped servers, in regions where the CDN is blocked, and behind strict Content-Security-Policy. Check the Blade view for a cdn.jsdelivr.net / unpkg reference. Mitigations:

  • Add https://cdn.jsdelivr.net to script-src and worker-src blob: (Monaco spawns Web Workers from blob URLs; without it you get a silent degradation to no language services).
  • Or vendor the Monaco distribution into public/ and point the loader at it. If the package has no config for this, you will be editing the published Blade view—budget for that.

Livewire state sync is the fragile seam. Monaco mutates the DOM behind Livewire's back. If a parent re-render wipes the container (a sibling field with ->live() that changes the schema, a tab switch, a $refresh), you can lose the cursor position or the buffer. Standard defences: wire:ignore around the editor, an Alpine component that owns the Monaco instance, and syncing via $wire.set('data.content', value, false) rather than a full round-trip on every keystroke. If you see "typing works but the value is empty on save", the sync direction is the culprit—inspect the x-init block first.

Debounce before you ->live(). Attaching ->live() to a Monaco field means an HTTP round-trip per keystroke batch. Use ->live(debounce: 750) or omit it entirely and let the field dehydrate on submit. Only go live if another part of the form genuinely depends on the content (e.g. a live preview pane).

Dark mode is not free. Filament's dark mode toggles a CSS class; Monaco's theme does not follow it automatically unless the package wires it up. If your panel supports theme switching, test it explicitly. The usual fix is a MutationObserver on document.documentElement that calls monaco.editor.setTheme()—add it to your panel JS if the package doesn't.

Large documents are slow. Monaco handles thousands of lines fine, but the Livewire payload does not. A 500 KB document will be serialised into every request. If the field holds genuinely large content, consider ->dehydrated() only on the save action, or store the value outside the Livewire component and use the editor purely as an input that POSTs once.

Testing: don't assert on the editor. Monaco never runs in Livewire/Pest tests. fillForm(['content' => '...']) still works because it sets the Livewire property directly—but it does not exercise the JS bridge, so a broken x-init will pass your entire test suite. Cover the round-trip once manually (or with a Dusk/browser test) and keep unit tests focused on validation and dehydrateStateUsing logic.

Assess the maintenance risk honestly. This is a ~19-star, single-maintainer package wrapping a fast-moving JS dependency inside a fast-moving PHP framework. Before committing:

  • Pin the version and read the composer.json constraint against your Filament major.
  • Check open issues for Filament-version breakage, especially around a Filament major upgrade.
  • Know your exit ramp: the field is thin, so replacing it with a custom Blade field wrapping Monaco directly is a day's work, not a rewrite. Keep the editor confined to a handful of forms so that swap stays cheap.
  • If you need to patch it, composer path-repository or a fork is preferable to editing vendor/—the latter evaporates on composer update.

Accessibility. Monaco is a keyboard-heavy component and traps Tab by default (tabFocusMode), which surprises keyboard-only users navigating a form. If accessibility is a requirement, set 'tabFocusMode' => true in options so Tab moves focus out, and always provide ->helperText() describing the expected format—the editor's own validation is visual only and screen-reader support for diagnostics is limited.

Debugging checklist, in order: browser console for 404s on the Monaco loader → network tab for the CDN request → wire:ignore/Alpine init in the Blade view → Livewire request payload (is the value actually being sent?) → Filament asset compilation (php artisan filament:assets, clear view:clear and browser cache). Empirically, the first two steps resolve most reports.

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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console