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 Editorjs Laravel Package

athphane/filament-editorjs

An Editor.js-powered rich text field for Filament and Laravel admin panels. Create block-based content with headers, lists, images, and other tools, store it as structured JSON, and render it cleanly on the frontend.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the package.

    composer require athphane/filament-editorjs
    

    Because this is a small, single-maintainer package, also confirm the supported Filament version in composer.json before committing to it — the package targets a specific Filament major (v3 at time of writing) and does not advertise wider compatibility.

  2. Publish assets if the package ships any.

    php artisan vendor:publish --tag="filament-editorjs-assets"
    php artisan vendor:publish --tag="filament-editorjs-config"
    

    If those tags don't exist, run php artisan vendor:publish interactively and pick the athphane/filament-editorjs provider entry. The published config (if present) is where you register custom Editor.js tools and upload disks.

  3. Look at the source first, not the README. With 18 stars and no dependents, the README is likely thin. Read in this order:

    • src/ — the Filament field class (extends Filament\Forms\Components\Field). This tells you the exact method names for configuring tools, profiles, and height.
    • resources/views/ — the Blade view wrapping the Editor.js mount point. This tells you which JS/CSS the package loads and whether it uses @vite, a published asset, or a CDN.
    • resources/js/ — the Alpine/Livewire glue. This is where state syncing happens and where most bugs will live.
    • config/ — upload disk, tool defaults.
  4. First use case — replace a RichEditor in a Resource form:

    use Athphane\FilamentEditorjs\Forms\Components\EditorJs;
    
    public static function form(Form $form): Form
    {
        return $form->schema([
            EditorJs::make('content')
                ->label('Body')
                ->required(),
        ]);
    }
    

    Pair it with a json or longText column (see Gotchas) and you have a working block editor in one field declaration.

  5. Verify the round trip before building further. Create a record through the form, inspect the raw DB value, and confirm it is Editor.js's { time, blocks, version } JSON — not HTML. That single check determines your entire frontend rendering strategy.

Implementation Patterns

Store JSON, render server-side or client-side — pick one and be consistent.

Editor.js's canonical output is a JSON document:

{ "time": 1710000000000, "blocks": [ { "type": "paragraph", "data": { "text": "…" } } ], "version": "2.28.0" }

The field will hand you that string. Two viable patterns:

Server-side rendering (recommended for SEO): decode in the model and render blocks with a Blade partial, or pipe through a PHP/JS Editor.js-to-HTML renderer.

// app/Models/Post.php
protected function casts(): array
{
    return ['content' => 'array'];
}
@foreach ($post->content['blocks'] ?? [] as $block)
    @switch($block['type'])
        @case('paragraph') <p>{!! $block['data']['text'] !!}</p> @break
        @case('header')    <h{{ $block['data']['level'] }}>{!! $block['data']['text'] !!}</h{{ $block['data']['level'] }}> @break
        @case('list')      <ul>@foreach ($block['data']['items'] as $i) <li>{!! $i !!}</li> @endforeach</ul> @break
        @case('image')     <img src="{{ $block['data']['file']['url'] }}" alt=""> @break
    @endswitch
@endforeach

The cast is the important part: it makes the field's JSON survive Livewire hydration/dehydration cleanly and gives you an array without json_decode calls scattered everywhere.

Client-side rendering: ship the same Editor.js bundle to the public site and use editor.save() → render(), or a library like editorjs-html. This keeps parity with the editor's output but costs you a JS bundle on the frontend and does nothing for crawlers.

Custom tools. Most real projects need more than paragraph/header. Register tools in the published config (or via a fluent ->tools([...]) method if the field exposes one) pointing at your tool's JS class name and any config:

'tools' => [
    'image' => [
        'class' => 'ImageTool',
        'config' => ['endpoints' => ['byFile' => route('editorjs.upload')]],
    ],
    'code' => \RawCodeTool::class,
],

Define the tool classes in a small resources/js/editorjs.js and load them through Filament's asset registration (FilamentAsset::register([...]) in a service provider) rather than editing the package's view — that keeps upgrades clean.

Uploads. Editor.js's ImageTool expects an HTTP endpoint returning { success: 1, file: { url: "..." } }. Wire it to a dedicated route with its own controller and Storage::disk(config('filament-editorjs.disk', 'public'))->put(...). Do not reuse Filament's FileUpload component internals; the contracts are unrelated.

Reactive / dependent forms. If the field sits next to something that changes its content (e.g. a template selector), use ->afterStateUpdated(fn (Set $set, $state) => $set('content', $state)) and make sure the field is ->reactive(). The Alpine component must re-mount when Livewire replaces the DOM — if the editor goes blank after a $wire.set, that's the root cause.

Multi-locale. Wrap per-locale instances in a Tabs/Grid and suffix the state path (content.en, content.fr). The package does not handle translations itself; it is one field, one JSON blob.

Validation. Add a custom rule that asserts the JSON decodes and each block has a type and data key. Editor.js will happily save an empty blocks: [] array, so ->required() alone will not stop blank submissions.

Gotchas and Tips

  • Column type matters. Editor.js JSON is verbose — a few images or long articles blow past VARCHAR(255) instantly. Use json or longText in the migration. json gives you validation at the DB level; longText is more forgiving across MySQL/MariaDB/SQLite.
  • ->required() does not work as you expect. The field's state is a JSON string that is always present ({"blocks":[]}), so required will pass on an empty editor. Validate the block count instead.
  • XSS is your problem, not the package's. Block data.text fields contain the raw HTML Editor.js emitted. Sanitize with a purifier (e.g. mews/purifier) before {!! !!} output. This is the single most common security bug in Editor.js integrations.
  • CSS/JS not loading in production. If the editor renders as an unstyled textarea, the package's assets aren't registered. Run php artisan filament:assets after install and after every composer update, and check php artisan filament:about (or the browser network tab) for the bundle.
  • Livewire + Editor.js is the fragile seam. Editor.js owns its DOM node; Livewire owns the surrounding DOM. Any full re-render of the form will destroy the editor instance. Mitigations: keep the field out of ->reactive() chains where possible, use wire:ignore in the package view (if it isn't already there, add it in a published override), and explicitly re-init on livewire:navigated.
  • Filament v2 vs v3 API drift. Field configuration methods differ (->rules() location, getStatePath(), Field namespace). Check composer.json constraints before assuming the README snippet applies; if you're on the other major, expect to fork or pin.
  • Two editors on one page. Multiple instances of the same field across tabs usually works only if each gets a unique DOM/CSS selector. If the second editor hijacks the first's content, this is why — the Alpine component's selector is not scoped per-instance, and it's a one-line fix in the package view.
  • Empty file on image blocks. If ImageTool's upload response shape is off by one key (file.url nested vs flat), the block saves but renders nothing on the frontend. Log the actual response once, early.
  • Debugging tip: in AppServiceProvider::boot(), dump() the field's dehydrated state with ->dehydrateStateUsing(fn ($state) => tap($state, fn ($s) => logger()->debug($s))) to see exactly what Filament is about to persist. Half of all "content disappeared" reports resolve here.
  • Extension points to prefer over patching: publish the Blade view (vendor:publish --tag=views) for wire:ignore/selector fixes, register tools via config rather than editing the package's JS, and register assets through a dedicated service provider. With a package this small and single-maintained, keeping your changes in app/ and resources/ is what makes composer update survivable.
  • Bus factor. One maintainer, 18 stars, zero dependents means no community backstop. If this goes on a production path, budget for reading resources/js/ end to end and pinning the version in composer.json ("athphane/filament-editorjs": "1.*" or an exact tag) rather than floating on dev-main.
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.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
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
cgv/localized-resource