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 Forms Tinyeditor Laravel Package

noin/filament-forms-tinyeditor

Integrates the TinyMCE rich text editor into Filament forms, giving admin panels a familiar WYSIWYG field with configurable toolbar, plugins, and content settings. Easily add rich text editing to Filament resources and custom forms.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Confirm compatibility before anything else. This package still has zero stars and zero dependents, so treat it as unvetted — but the release history is healthier than a single drop: v4.0.4 lands on top of at least v4.0.3, so there is some upstream iteration. Check composer.json on the repo for the filament/filament constraint and match it to your app's Filament major (v3 vs v4 field APIs differ significantly — Filament\Forms\Components\Field vs Filament\Schemas\Components\Component). If the constraint is missing or *, pin it yourself ("noin/filament-forms-tinyeditor": "^4.0") and read the source.

2. Install and register assets.

composer require noin/filament-forms-tinyeditor

Most TinyMCE wrappers for Filament ship a service provider that auto-registers via package discovery. Verify it appears in bootstrap/providers.php (Laravel 11+) or config/app.php. If the package publishes assets, run:

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

3. Where to look first. In order of value:

  • src/ — the field class itself. This is a thin wrapper; you want to see exactly which TinyMCE init options it hardcodes and whether it uses the TinyMCE Cloud CDN (requires an API key, has domain allowlisting and a nag banner without one) or a self-hosted bundle. As of v4.0.4 also look for the new focus-management / "shield" code and whether it's configurable or always-on.
  • resources/views/ — the Blade view holding the <textarea>/<div> plus the Alpine x-data wiring. This tells you how state syncs to Livewire.
  • resources/js/ — whether there's a custom Alpine component or a raw tinymce.init() call. The latter is the usual source of re-initialization bugs. The v4.0.4 focus/shield work will live here.
  • config/ — toolbar presets, height, plugins list, upload settings, and whatever knobs were added for focus/shield.

4. First use case. The canonical fit is a CMS-style admin panel: a content or body column on a Post/Page model rendered as rich HTML.

use Noin\FilamentFormsTinyeditor\TinyEditor; // confirm namespace in src/

TinyEditor::make('body')
    ->label('Body')
    ->columnSpanFull()
    ->required(),

Pair it with a migration using longText (TinyMCE output easily exceeds TEXT's 64KB once you embed base64 images or long tables) and render with {!! $post->body !!} only after sanitization (see Gotchas).

Start with the smallest possible field on a single resource, verify the editor loads and saves in the browser, then configure. Debugging toolbar config and asset loading at the same time is miserable.

Implementation Patterns

Centralize configuration in a config file or a base class. Repeating a 30-item toolbar array and plugin list across every resource guarantees drift. Two workable approaches:

Config-driven — publish the config and set one default profile, then override per-field only when needed:

// config/filament-forms-tinyeditor.php (shape depends on package)
return [
    'profile' => 'default',
    'profiles' => [
        'default' => [
            'plugins' => 'lists link table code wordcount',
            'toolbar' => 'undo redo | bold italic | bullist numlist | link table | code',
            'menubar' => false,
            'height' => 500,
        ],
        'minimal' => [
            'plugins' => 'lists link',
            'toolbar' => 'bold italic | bullist numlist | link',
            'menubar' => false,
            'height' => 200,
        ],
    ],
];

Base-class driven — cleaner typed ergonomics:

namespace App\Filament\Fields;

class RichText extends \Noin\FilamentFormsTinyeditor\TinyEditor
{
    protected function setUp(): void
    {
        parent::setUp();

        $this->profile('default')
             ->columnSpanFull();
    }
}

// usage
RichText::make('body')

Two profiles, not ten. Real panels typically need exactly two: a full authoring profile (tables, code view, media) and a compact one for short descriptions, callouts, or sidebar text. Anything in between is usually a product decision masquerading as a config option.

Know what v4.0.4's focus management and "shield" actually do. The changelog entry is one line, so read resources/js/ before assuming behaviour. Two possibilities, with different consequences:

  • Focus trap / escape guard — keeps Tab from walking out of the editor iframe into Filament's chrome, and stops the browser from stealing focus back mid-edit. This is genuinely useful inside modals and slide-overs, where Tab order gets messy. If it's on by default, verify Tab still reaches your modal's action buttons after the editor; an over-aggressive trap is a real accessibility regression.
  • A wrapper around a premium/marketplace TinyMCE plugin — if so, confirm licensing, because a "shield" that quietly requires a TinyMCE subscription changes your cost story (see the API-key gotcha below).

Either way, check whether the feature is exposed through the field's option passthrough. If it's hardcoded on, that's a fork waiting to happen; if it's configurable, add it to your base class so the behaviour is identical across every editor in the panel.

Pair with a file-upload story early. TinyMCE's image and media dialogs are useless without an upload endpoint. Two paths:

  • Simple/cheap: allow URL insertion only (image_list, file_picker disabled). Zero backend work, but authors paste external hotlinks that rot.
  • Durable: implement TinyMCE's expected upload contract. The editor's images_upload_handler (JS) or images_upload_url (form POST) must return { "location": "https://.../image.jpg" }. Point it at a signed, authenticated Filament/Laravel route that stores to your public or S3 disk and returns the URL. Do not expose an unauthenticated upload endpoint.
// routes/web.php — behind auth + your panel middleware
Route::post('/admin/tinymce/upload', [TinyMceUploadController::class, 'store'])
    ->middleware(['web', 'auth'])
    ->name('tinymce.upload');

Validate MIME type and size server-side; the client-side valid_elements and file-picker filters are cosmetic only.

Store HTML, render sanitized. Persisting raw editor HTML and echoing it unescaped is the standard XSS vector in this class of package. Two layers:

// sanitize on save (mutator or form ->mutateStateForSave)
->mutateStateForSave(fn (?string $state) => app(Sanitizer::class)->clean($state ?? ''))

// or sanitize on render
{!! clean($post->body) !!} // mews/purifier / stevebauman/purify

Sanitize on save so your API, emails, and Blade partials all get the same trusted value; sanitizing only at one render site leaves the others vulnerable.

Integrate with Filament's rich-content ecosystem. If you're tempted to add this to a resource that already uses RichEditor (Filament's built-in Tiptap editor), stop — mixing two rich-text components in one panel means two toolbars, two asset bundles, and inconsistent paste behaviour. Pick one per panel. The legitimate reasons to reach for TinyMCE are: authors demand specific TinyMCE plugins (e.g. table cell merging, advanced paste-from-Word), the client already has TinyMCE Cloud licensing, or a migration from a legacy TinyMCE backend.

Form state and reactivity. Use standard Filament field chaining; the wrapper delegates to the field API. Common patterns:

TinyEditor::make('body')
    ->profile('default')
    ->columnSpanFull()
    ->required()
    ->maxLength(65535)         // validate against your longText column
    ->helperText('Markdown not supported — use the toolbar.'),

->live() will work but each keystroke-triggered round trip is wasteful; debounce or rely on save-time state instead.

Testing. Feature tests can ->fillForm(['body' => '<p>Hello</p>']) and assert the model persisted it; the editor JS never runs in tests, which is exactly what you want. Add a browser test (Filament's testing helpers / Dusk / Pest browser) for the one thing feature tests can't cover: that the editor initializes and syncs its content into Livewire on a real page load. Since v4.0.4 touches focus handling, extend that browser test to cover keyboard navigation — Tab into the toolbar, Tab out to the form's save button — and one run inside a modal. That's the regression surface this release just widened.

Tabbed and modal contexts. If your form uses Tabs or ->modal(), place the editor on a tab and confirm it initializes when the tab becomes visible. TinyMCE measures its container on init; hidden containers yield a zero-height editor that only recovers on resize. v4.0.4's focus work may improve focus-on-open but does not automatically solve zero-height measurement — still add a visible hook that calls tinymce.get(id)?.mode.set('design') / a resize refresh if the package doesn't.

Gotchas and Tips

The TinyMCE API key is a real cost/breakage point. If the package loads TinyMCE from cdn.tiny.cloud, you need a valid api_key in config. Without one you get a "This domain is not registered" notice or a toolbar that silently fails. Free tiers display a nag banner and are domain-scoped — a preview/staging domain will break while production works, or vice versa. If the package ships no self-hosted bundle, budget time to add one (npm tinymce + tinymce-i18n, publish to public/js, or a Vite entry) or reconsider the dependency. The v4.0.4 "shield" feature is worth checking against this: if it depends on a premium Cloud plugin, your free-tier key won't cover it.

Low adoption, short release history → still audit the JS. A v4.0.4 patch proves the maintainer is alive, which is better than a dead single release — but with zero dependents nobody else has found the bugs for you. Skim the view file for: hardcoded tinymce.init without tinymce.remove() on teardown, missing wire:ignore on the container, and content synced on change but not blur. Any of those causes duplicated editors on Livewire navigation or lost final keystrokes. Because the newest change is focus code, pay specific attention to whether it steals focus from Filament widgets (date pickers, searchable selects) that open as portals/overlays.

Livewire re-initialization is the #1 bug. Alpine/Livewire re-renders will re-run tinymce.init() on a container that already has an instance → duplicate toolbars, or the editor resets to the initial server-side value. Required fixes: wrap the editor container in wire:ignore, and call tinymce.remove('#selector') (or the package's cleanup hook) in the destroy/x-on:livewire:navigating handler. If the package doesn't do this, you'll be patching the Blade view yourself — expect it as a fork, and note that a fork means you're now maintaining a package with near-zero upstream activity. Re-check this after upgrading, since new focus/shield listeners are exactly the kind of thing that leaks across Livewire navigations.

HTML round-tripping mutates your content. TinyMCE normalizes markup against its valid_elements/valid_children schema. Content saved as <div class="callout"> can come back stripped or re-nested, and pasted Word HTML balloons into inline styles. Symptoms: git diff on content files shows churn after every save, or a frontend component that keys off a specific tag stops matching. Mitigations: widen valid_elements for tags you rely on, enable paste_as_text or the powerpaste plugin for author paste, and consider storing the editor output plus a sanitized/derived version rather than trusting the round-trip to be lossless.

Don't validate rich text by length alone. ->maxLength(65535) is a database-safety check, not a content rule — TinyMCE adds <p> wrappers, attributes, and inline styles, so a 400-word post can be 40KB. Validate character count on strip_tags($state) via a custom rule if you have real limits, and use longText/mediumText per your expected ceiling. If you enable base64 image embedding (avoid it — it also destroys browser performance), no realistic column size is safe; force uploads instead.

Dark mode and panel theming. TinyMCE has its own skin (oxide light/dark). Filament panels often use a custom theme + dark mode toggle; the editor will stay light unless the package maps Filament's theme to the TinyMCE skin/content_css config, or you pass content_css: 'dark'. Expect a white rectangle in dark mode on first install — check the config for a skin option, and if absent, add it via the component's config-merge method.

RTL and localization. TinyMCE ships separate language packs (tinymce-i18n); a self-hosted bundle without them defaults to English. RTL requires both the language pack and directionality: 'rtl'. Filament panels with ->locales() or Arabic/Hebrew content will otherwise render LTR-justified text in an RTL layout. Note that new focus-management code sometimes hardcodes left/right caret assumptions — retest in an RTL locale after upgrading.

Extension points to look for. Before forking, check the field class for a config-merge or option-passthrough method. Common shapes:

TinyEditor::make('body')
    ->profile('default')
    ->editorOptions([          // arbitrary TinyMCE init options
        'paste_as_text' => true,
        'toolbar_mode' => 'sliding',
        'content_style' => 'body { font-family: Inter, sans-serif; }',
    ]);

If nothing like this exists, check for protected properties / macro() support. Filament components are macroable in most versions; a TinyEditor::macro() registered in a service provider is often the least-invasive way to inject defaults across all fields without forking, though macros can't reach setUp()-level config — and they definitely can't reach listeners wired in the package's JS, so focus/shield behaviour is config-or-fork only.

Watch for the Filament v4 upgrade. Filament v4 moved form components under Filament\Schemas. A package pinned to v3 will break on upgrade with a class-not-found in setUp(). The 4.x version series suggests the package now tracks Filament v4, but confirm the constraint rather than inferring from the version number — a wrapper's own major doesn't have to match Filament's. Test it on a branch before adopting, and if it breaks, that fork is yours to own.

Escape hatch. If TinyMCE proves more trouble than it's worth (the common outcome when the motivation is "authors want word-processor paste"), the alternatives are Filament's built-in RichEditor, or dropping to a plain <textarea> field plus a Markdown pipeline — both far better supported than a low-adoption wrapper. Decide this before building a content workflow around it, and reassess if the focus/shield additions turn out to be hardcoded rather than configurable.

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