wiebenieuwenhuis/filament-code-editor
A Filament form field that adds a full-featured code editor powered by CodeMirror, with syntax highlighting, themes, and configurable modes for editing code, JSON, and other languages directly inside your Filament forms and panels.
Install and publish assets. The package ships JS/CSS for its editor (a CodeMirror-based Alpine component). After Composer install you must make those assets available to Filament:
composer require wiebenieuwenhuis/filament-code-editor
php artisan filament:assets # Filament v3 asset pipeline
# if the tag exists in your version:
php artisan vendor:publish --tag="filament-code-editor-assets"
Where to look first. Open vendor/wiebenieuwenhuis/filament-code-editor/src/Components/CodeEditor.php. That single class is the public API — the fluent methods you see there (language(), height(), and whatever else your pinned version exposes) are the whole surface area. Read the README's example form, then read the class, and you're done; there is no service provider config worth studying beyond asset registration.
Minimal first use case. Drop it into any Resource form in place of a Textarea, bound to a TEXT/LONGTEXT column:
use Wiebenieuwenhuis\FilamentCodeEditor\Components\CodeEditor;
CodeEditor::make('snippet')
->language('php')
->height('400px')
->columnSpanFull();
The field's state is a plain string, so persistence, ->required(), ->maxLength(), and Filament's normal validation all work unchanged. A good first target is a "custom CSS" or "webhook payload template" field on a Settings page — small blast radius, obvious win over a textarea, and it lets you confirm the assets actually load before you refactor a dozen forms.
Language per record, not per form. Hard-coding ->language('php') is fine for a one-off, but if users store multiple snippet types, drive it from the model:
CodeEditor::make('body')
->language(fn (?Snippet $record) => $record?->language ?? 'plaintext')
Wrap this behind a custom field class (extend CodeEditor, add a snippet() helper) so the ten forms in your app don't each re-derive the language. This is also your escape hatch if the package dies — you swap one parent class.
Pair the editor with a preview + hard validation. The editor is a UI affordance, not a validator. For JSON, validate in ->rules() with json_decode + json_last_error(); for PHP, shell out to php -l in a custom rule or accept that you're storing unvalidated code. Rendering the stored value back to users requires {{ }}/e() escaping — never {!! !!} a field a user typed into this editor.
Read-only for non-editors. Filament's ->disabled()/->readOnly() generally propagate to the underlying Alpine component, but verify visually — a disabled CodeMirror instance that still accepts keystrokes is a common gap in small packages. If it misbehaves, fall back to ->hidden(fn () => ! auth()->user()->can('edit')) plus a TextEntry-style display.
Settings pages and modals. Because it is a standard Filament field, it composes into filament/spatie-laravel-settings-plugin pages and action modals without ceremony — but modal usage is where asset/height issues show up first (the editor can render at 0px if the modal measures before mount). Set an explicit ->height() rather than relying on CSS defaults.
Testing. Treat it as a text field in tests: fillForm(['snippet' => '<?php echo 1;']) on a Livewire::test() of the Resource page works because state flows through Livewire's normal set/dehydrateStateUsing path. Don't try to simulate the CodeMirror DOM in Dusk unless you genuinely need to test the JS — assert on the persisted column instead.
Pair with a diff/versioning story. The moment users edit code through a browser form, "who broke prod" becomes your problem. Store a revision (or use owen-it/laravel-auditing) on save. This costs an hour now and is the difference between the field being a convenience and a liability.
php artisan filament:assets and php artisan optimize:clear. Deploying with a stale public/ directory (or a deploy script that skips asset publishing) silently degrades to a plain textarea — which looks "fine" until someone notices there's no syntax highlighting.eval-free but dynamic style elements. A nonce-based CSP will blank the editor with no console-obvious cause unless you allow style-src 'unsafe-inline' or nonce the generated <style> tags. Test the editor on a production CSP, not just locally.TEXT caps at 64KB. CSS/JS bundles and large JSON fixtures blow past that quietly (MySQL truncates or errors depending on STRICT_TRANS_TABLES). Use LONGTEXT and consider a ->maxLength() that matches reality.->language() argument is the first suspect — check the package's supported-language list against your value.wire:model.live. Large files plus live binding equals input lag; the editor syncs state on debounce. Keep it as the default (deferred) binding and only switch to .live for small snippets where you need reactive previews.composer.json, and write a 20-line test that mounts a form containing the field so a Filament major bump fails loudly in CI rather than in a user's browser. The field class is thin enough that forking is a realistic — and probably eventual — outcome; keep package-specific calls isolated in one custom field class to make that swap cheap.How can I help you explore Laravel packages today?