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

Livewire Jodit Text Editor Laravel Package

mantix/livewire-jodit-text-editor

A Livewire 3/4 WYSIWYG rich text editor component built on Jodit. Drop it into your Blade views for a full-featured editor with customizable toolbars, multiple editor support, and programmatic content updates.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and get assets loaded in the right order.

composer require mantix/livewire-jodit-text-editor

Then pick your asset path. For anything that will reach production, use NPM — the CDN snippet in the README is an unpinned third-party runtime dependency:

npm install jodit
// resources/js/app.js
import 'jodit/esm/plugins/resizer/resizer'; // needed for image resize handles
import 'jodit/esm/plugins/video/video';     // needed for video embeds
import { Jodit } from 'jodit';
window.Jodit = Jodit;
// resources/scss/app.scss
@import 'jodit/es2021/jodit';

The single thing that will bite you on day one: window.Jodit must exist before the Livewire component initialises. Import it from app.js (which Vite loads via @vite in your layout head) — do not put the Jodit <script> tag after @livewireScripts and expect it to work. If you go the CDN route for a spike, put the <link>/<script> tags in <head> above @vite/@livewireScripts.

Where to look first.

  1. README Basic Usage and Customization — that's 90% of the API surface (three attributes and a publish tag).
  2. The changelog. It's short and it's the best map of where the seams are: v1.8 and v1.10 both exist to fix wire:ignore / wire:navigate duplicate-editor behaviour, and v1.10 added the options passthrough. Those two entries tell you what to regression-test.
  3. Publish the view immediately and read it, even if you never edit it:
    php artisan vendor:publish --tag=livewire-jodit-text-editor-views
    
    The generated file at resources/views/vendor/livewire-jodit-text-editor/livewire/jodit-text-editor.blade.php is the authoritative answer to "what event names, what wire:ignore scope, what DOM id does it actually use?" — much faster than guessing from the README.

First use case — one field on one admin form.

// app/Livewire/Admin/EditPost.php
public Post $post;
public string $content = '';

public function mount(Post $post): void
{
    $this->post = $post;
    $this->content = $post->content ?? '';
}

public function save(): void
{
    $this->validate(['content' => ['required', 'string', 'max:65535']]);
    $this->post->update(['content' => clean($this->content)]); // sanitize on write
    $this->dispatch('saved');
}
<livewire:jodit-text-editor wire:model.live.debounce.500ms="content" />

Add .debounce from the start. wire:model.live alone fires a server round-trip per editor change event, which on a document of any length is both slow and noisy in your logs.

Implementation Patterns

Treat it as a form control, not a subsystem. The integration is one Blade component with a normal Livewire property behind it. Everything you already do for wire:model fields — $this->validate(), assertSet() in tests, updated() hooks — keeps working. Don't build a wrapper layer around it; do keep the tag confined to a handful of Blade partials so an editor swap stays cheap.

Toolbar presets rather than inline arrays. You will end up with three or four toolbar shapes across the app. Define them once and pass them through:

// config/editors.php
return [
    'minimal'  => ['bold', 'italic', 'underline', '|', 'link', 'eraser'],
    'standard' => ['bold', 'italic', 'underline', 'strikeThrough', '|', 'ul', 'ol', '|', 'left', 'center', 'right', '|', 'link', 'image'],
    'full'     => ['*'],
];
<livewire:jodit-text-editor
    wire:model.live.debounce.500ms="content"
    :buttons="config('editors.standard')"
    :options="['height' => 480, 'placeholder' => 'Write the post body…', 'toolbarSticky' => true]"
/>

The options attribute (v1.10) is a raw passthrough to Jodit's config object — height, placeholder, readonly, toolbarSticky, uploader, imageDefaultWidth all work. Invalid keys are ignored silently, so verify against Jodit's config docs rather than assuming a typo will throw.

Multiple editors — always set identifier. The identifier is what makes programmatic updates and event targeting safe on a page with more than one editor. Adopt a naming convention ({field}-editor, {model}-{field}-editor) so the strings in PHP and Blade don't drift.

@foreach ($blocks as $block)
    <livewire:jodit-text-editor
        wire:key="block-{{ $block->id }}"
        wire:model.live.debounce.500ms="blocks.{{ $loop->index }}.body"
        identifier="block-{{ $block->id }}-editor"
        :buttons="config('editors.minimal')"
    />
@endforeach

The wire:key is not optional in a loop — without it Livewire reuses component instances on reorder and you get content appearing in the wrong editor.

Programmatic writes via the dispatch event. This is the escape hatch for "load a template", "insert a saved snippet", "reset on cancel":

// target one editor
$this->dispatch('update-jodit-content', ['content-editor', $this->content]);

// target every editor on the page
$this->dispatch('update-jodit-content', $this->content);

Note the two different payload shapes — a scalar means "all editors", an array [identifier, content] means "just this one". Use the array form by default; the broadcast form is a footgun on multi-editor pages.

Sanitize on write, escape on read. Jodit is an editor, not a sanitizer. The package ships no purification at all. Wire in mews/purifier (or symfony/html-sanitizer) in a form request or a model mutator, with an allow-list covering exactly the tags your toolbar can produce:

'content' => ['required', 'string', 'max:65535'],
// then
$post->content = Purifier::clean($this->content, 'editor');

Then audit every {!! $post->content !!} in the codebase. If any of them render a field editable by a role you don't fully control, that's the launch blocker — not the editor choice.

Media uploads: budget a spike. The README shows how to load the resizer and video plugins but never defines an upload endpoint contract. The intended shape is Jodit's own uploader config passed through options, pointing at a route you own:

:options="[
    'uploader' => [
        'url' => route('admin.editor.upload'),
        'format' => 'json',
        'headers' => ['X-CSRF-TOKEN' => csrf_token()],
    ],
]"

Your endpoint must return Jodit's expected envelope (success, files, path, baseurl) and must enforce auth, size limits, and MIME validation independently — the client-side accept list is not a security control. Confirm the exact response shape against the Jodit version you pinned before you build anything on top of it.

Livewire wire:navigate pages. Because the changelog history here is littered with duplicate-editor fixes around wire:ignore, add a browser check for "navigate away and back" to your test plan for any page hosting an editor. This is the single most fragile interaction in the package.

Testing. PHPUnit can only see the property: Livewire::test(EditPost::class)->set('content', '<p>x</p>')->call('save')->assertSet(...). That covers validation and sanitization. The editor's JS lifecycle needs a Dusk/browser test — one test asserting that two editors on a page don't cross-contaminate and that update-jodit-content with an identifier only touches the intended one.

Gotchas and Tips

The package does not pin Jodit — you do. The README demonstrates 4.1.16 via unpkg. If you use the CDN path you've inherited a floating third-party script with no lockfile. Pin exact versions in NPM, and treat a Jodit major bump as a regression-test item, not a routine npm update.

The update event is a global window event, not a component method. It has no return value, no completion callback, and no ordering guarantee relative to Livewire's own re-render. Dispatching it and immediately re-rendering the parent in the same action can leave you with stale or duplicated editors. If you need "set content, then focus", chain it through $this->js() rather than assuming sequencing.

Don't scatter the event name. update-jodit-content is a magic string. Wrap it in a trait method (updateEditor(string $identifier, string $html)) or a single Blade partial. That's the difference between a two-hour editor swap and a two-day one.

Tailwind Preflight will strip your list styles. Tailwind's base reset removes ul/ol markers and heading sizes, and Jodit's editable area is regular DOM — so the editor renders differently than the saved output, and both look wrong. Fix with a scoped .jodit-wysiwyg / content-wrapper rule, or apply prose to the editor content container specifically. Also watch for global * resets colliding with Jodit's toolbar layout.

Button names are Jodit's, not yours. | is a separator, * means all. A misspelled button name doesn't error — it just doesn't appear. Check Jodit's button list, and when something is missing from the toolbar, confirm the JS plugin for it is loaded (this is why image and video can vanish: the resizer/video plugins weren't imported into window.Jodit).

Debugging: go through the global. window.Jodit.instances (and Jodit.instances[...].value) is the fastest way to see whether the editor initialised, which identifiers exist, and whether content actually landed. If the editor doesn't appear at all, it's almost always script load order or window.Jodit being undefined at hydration time.

Prefer .blur over .live for long documents. Every live sync ships the full HTML body to the server, which grows non-linearly with document length — a 40KB document syncing per debounce is a real cost. wire:model.blur="content" plus an explicit "save" is often the better UX and a much better payload profile.

Never use the editor for read-only display. Render sanitized HTML directly. Spinning up Jodit to show content is a needless JS payload, a needless XSS surface, and it drags the whole asset bundle onto pages that don't need it.

Extension points, in order of safety: the options attribute (pure config), :buttons (config), publishing and editing the Blade view (safe, contained — but note you're now maintaining a fork of that file and upgrades will conflict), and finally patching the JS inside the published view (last resort; document it). The published view is genuinely the escape hatch for anything the README doesn't expose — it's just Blade plus a <script>, so toolbar event hooks or a custom uploader callback belong there rather than in a wrapper component.

Upgrade checklist, given a 31-star package with a single maintainer: re-run the navigate-away-and-back browser test, the two-editor isolation test, and the programmatic-update test on every version bump. That's a fifteen-minute suite that catches essentially all of the regressions this package has historically shipped.

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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core