open-kampus/filament-ace-editor
Ace Editor field for Filament forms, adding a full-featured code editor with syntax highlighting and themes to your admin panels. Edit JSON, HTML, CSS, PHP and more directly in Filament resources, with configurable editor options and a simple, drop-in form component.
open-kampus/filament-ace-editor adds an Ace code editor form component to Filament panels — the fastest way to replace a plain Textarea when users edit JSON, CSS, JS, HTML, Blade, SQL, or YAML inside an admin resource.
Install
composer require open-kampus/filament-ace-editor
Version 1.1.2 adds Laravel 12 support, so it's a safe bump if you're moving a Filament v3 panel onto a Laravel 12 host. It is a compatibility release — no new API surface.
Publish Filament assets (the JS/CSS is registered through Filament's asset pipeline, not auto-injected):
php artisan filament:assets
php artisan optimize:clear
First use case — a settings/resource form where a content column holds raw CSS or JSON:
use OpenKampus\FilamentAceEditor\Forms\Components\AceEditor;
AceEditor::make('content')
->mode('css')
->theme('monokai')
->height('420px')
->columnSpanFull();
Where to look first:
mode(), theme(), plus an options passthrough — confirm exact names against the installed version).src/Forms/Components/AceEditor.php — all available setters and their defaults.ace.edit() init call, and it's the file you'll touch if you need behavior the fluent API doesn't expose.Keep the mode/theme per field, not global. A single panel often needs several: json for config blobs, php_laravel_blade for templates, css for theming, sql for report queries. Set them inline so the intent is readable at the call site.
Make the database value and the editor value agree. Ace gives you a raw string; your column may be JSON or an array cast. Normalize both directions:
AceEditor::make('payload')
->mode('json')
->formatStateUsing(fn ($state) => is_array($state)
? json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)
: (string) $state)
->dehydrateStateUsing(fn ($state) => filled($state) ? json_decode($state, true) : null)
->rules(['nullable', 'json']);
The json rule plus the dehydrate hook is usually enough; you don't need a custom validator, but do validate — Ace will happily let a user save a trailing comma.
Use it as a read-only viewer on View pages. Rather than duplicating the field, gate it:
AceEditor::make('content')
->mode('html')
->disabled(fn (string $operation) => $operation === 'view');
Combine with ->columnSpanFull() and an explicit ->height(), otherwise the editor collapses into a short box inside a two-column grid. Disabled state is also how you avoid the editor swallowing clicks meant for the surrounding form.
Be deliberate about reactivity. The editor is a JS widget syncing into Livewire state. ->live() on every keystroke will round-trip the whole component per character and can cause cursor jumps or re-init flicker. Prefer ->live(onBlur: true) or ->live(debounce: 800) when you need dependent fields (e.g. a live JSON preview). If you only need the value on submit, don't make it live at all.
Repeaters and Builders work, but verify re-init. When a repeater row is added, the editor container is re-rendered; the package's Alpine hook is responsible for re-mounting Ace on the new node. Test adding 2–3 rows and editing in each — this is the most common place a field-wrapper package breaks, and it's cheap to check before shipping.
Testing: your Pest/PHPUnit form tests never execute Ace. Call ->fillForm(['content' => '...'])->call('save') and assert on the persisted value and validation errors only. Don't assert on rendered editor markup — it's brittle and tells you nothing.
filament:assets after every composer update. Ace's JS won't load otherwise and you'll get an empty grey box with no console error from Laravel's side. Same fix for the classic "editor rendered fine yesterday" after a deploy that skipped asset publishing.ace-builds locally or references a CDN. If you're deploying to an air-gapped or CSP-restricted environment, this matters — Ace's workers and some modes use blob/eval-like constructs that a strict script-src policy will block. Test in the target environment, not just locally.php artisan view:clear resolves most "my options aren't taking effect" reports; the options are baked into the view at render time.theme() is fixed — it won't follow the panel's dark toggle. Either pick a theme that reads acceptably in both, or bind it: ->theme(fn () => filament()->getCurrentPanel()->hasDarkModeForced() ? 'monokai' : 'chrome'). Also keep the editor background consistent with your panel or the field will look pasted-in.'' for null state. Pair formatStateUsing with filled() checks if the column is nullable and you have a json rule, otherwise an untouched required-looking field fails validation on save.->maxLength() or move genuinely large content to a dedicated page.AceEditor and override the view for a reusable preset (e.g. a BladeEditor with mode + theme + height pre-baked); (3) fork the Blade view if you need custom JS events (ace.edit() callbacks, on('change') hooks tied to Livewire). Subclassing is the cleanest and survives package updates far better than overriding the view.How can I help you explore Laravel packages today?