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

Nova Froala Field Laravel Package

michielkempen/nova-froala-field

Laravel Nova field that integrates the Froala WYSIWYG editor for rich-text content in Nova resources. Adds an easy way to use Froala editing in forms with configurable editor options for managing HTML content.

View on GitHub
Deep Wiki
Context7

Getting Started

Requirements. Laravel Nova (3.x or 4.x — confirm the version tag matches your Nova major), PHP 8.0+, and a TEXT/LONGTEXT column on the model. The package is a Nova field only; it does not alter your schema.

Install.

composer require michielkempen/nova-froala-field

There is no service provider to register manually under Nova's auto-discovery. If you upgrade, rerun composer dump-autoload.

First use case — replace a plain textarea on a Post model.

use MichielKempen\NovaFroalaField\Froala;

public function fields(Request $request)
{
    return [
        Froala::make('Body')
            ->rules('nullable', 'string', 'max:65535'),
    ];
}

Make sure the migration used $table->longText('body')->nullable() — a string column will silently truncate at 255 chars in MySQL strict mode off, and explode in strict mode.

Where to look first.

  1. The package's README.md — the toolbar/options/withFiles examples are the only real API docs.
  2. vendor/michielkempen/nova-froala-field/src/Froala.php — this is the entire surface area. It's small; read it end-to-end before you write any wrapper.
  3. dist/js/field.js — grep for FroalaEditor to see which Froala version is bundled. That version determines which option names are valid (Froala 3 renamed several).
  4. Nova's own Field base class for the inherited methods (rules, help, onlyOnForms, hideFromIndex, resolveUsing, etc.).

Sanity check before writing fields. Open a Nova create form, confirm the toolbar renders, and check the browser network tab for the Froala JS/CSS assets. If the editor is a blank textarea, the assets did not load — that is the single most common first-run failure.


Implementation Patterns

Toolbar configuration. Pass a flat array for a single row, or a nested array to build button groups separated by dividers:

Froala::make('Body')
    ->toolbar([
        ['bold', 'italic', 'underline', 'strikeThrough'],
        ['formatBlock', 'fontSize', 'color'],
        ['insertImage', 'insertLink', 'insertTable'],
        ['undo', 'redo', 'html'],
    ]);

Arbitrary Froala options. Anything not exposed as a fluent method goes through ->options(). This is the escape hatch for height, placeholder, language, sticky toolbar, and the commercial license key:

Froala::make('Body')
    ->options([
        'heightMin'    => 320,
        'placeholder'  => 'Write the post…',
        'toolbarSticky' => true,
        'language'     => 'en_gb',
        'key'          => config('services.froala.key'),
    ]);

File and image uploads. Enable the upload plugins and point them at a disk:

Froala::make('Body')
    ->withFiles('public')
    ->options([
        'imageUploadURL'    => '/nova-vendor/froala/upload', // confirm path in README
        'imageAllowedTypes' => ['jpeg', 'jpg', 'png', 'gif', 'webp'],
    ]);

Run php artisan storage:link and verify the disk has 'visibility' => 'public' in config/filesystems.php; otherwise the uploaded image URL resolves to a 403.

Index and detail display. HTML tags in an index cell are ugly and, worse, are rendered as escaped text. Truncate to plain text on the index:

Froala::make('Body')
    ->hideFromIndex(),

Text::make('Body')
    ->onlyOnIndex()
    ->displayUsing(fn ($value) => Str::limit(strip_tags($value ?? ''), 80))
    ->asHtml(),

Centralize the config in a custom field. Don't repeat the options array across five resources. Subclass once:

namespace App\Nova\Fields;

use MichielKempen\NovaFroalaField\Froala;

class RichText extends Froala
{
    public function __construct($name, $attribute = null, $resolveCallback = null)
    {
        parent::__construct($name, $attribute, $resolveCallback);

        $this->toolbar(config('editor.toolbar'))
            ->options(config('editor.options'));
    }
}

Now every resource gets the same editorial experience and a single place to change it.

Sanitize on the way in and out. The field writes raw HTML. Strip dangerous tags at the model boundary so imports, seeders, and API writes are covered too:

// app/Models/Post.php
protected function body(): Attribute
{
    return Attribute::make(
        set: fn (?string $value) => $value === null
            ? null
            : Purifier::clean($value, 'post'),
    );
}

Then render with {!! $post->body !!} only because you know the model already cleaned it.

License key from config, never inline. Publish or create config/services.php:

'froala' => [
    'key' => env('FROALA_KEY'),
],

and reference it in ->options(['key' => ...]). This lets staging and production use different keys, and keeps the key out of git.

Testing. Nova fields are testable in isolation with Nova's field test helpers:

Froala::make('Body')->toolbar(['bold'])
    ->assertJsonContains(['toolbar' => ['bold']]); // shape depends on field meta

Assert on field->meta rather than rendering Vue — you cannot meaningfully assert on the editor's DOM without Dusk.


Gotchas and Tips

Froala is commercially licensed. The bundled editor runs unlicensed in development (with a watermark or console warning) and you must supply a key option for production. Check which Froala major is bundled in dist/js/field.js — Froala 3/4 changed option names (toolbarButtons vs toolbar, imageUploadURL semantics, plugin registration). If a documented option silently does nothing, version mismatch is the reason.

->options() merges shallowly. Nested arrays (like an explicit toolbarButtons or events) are replaced wholesale, not merged key-by-key. If you set toolbar via both ->toolbar() and ->options(), the second call wins — pick one.

Closures will not survive serialization. Field meta is JSON-encoded to the browser. Anything you pass into ->options() must be JSON-safe: no closures, no objects like UploadedFile, no Carbon instances. For event handlers ('events' => ['initialized' => ...]), build a small custom Nova tool or inject the script with Nova::script(); you cannot pass a PHP closure through.

Conditional forms break the editor. When a Froala field sits behind ->dependsOn() or inside a FormPanel that mounts hidden, Froala initializes against a zero-width container and shows a collapsed toolbar. Fix by forcing re-initialization on the Vue $nextTick in a custom component, or avoid dependsOn for the rich-text field.

Dark mode looks broken out of the box. The editor is an <iframe> and does not inherit Nova's Tailwind dark palette. The content area is white with dark text, which is fine, but the wrapper/placeholder text can be unreadable. Inject a small CSS override in your Nova custom stylesheet targeting .fr-element and .fr-toolbar.

Storing absolute URLs pins you to a disk. Uploaded images are saved as full URLs by default. If you plan to move from local to S3, either configure relative-path storage in the field options or migrate values with a regex. Decide before content goes live.

Model casts bite. body should be a plain string/longText. If the attribute is cast to array or json, the editor receives a JSON string and you get [ on the first keystroke.

Validation is on bytes, not characters. max:65535 matches a MySQL TEXT column, but HTML markup counts — a 60 KB article with image tags can hit the limit. Use LONGTEXT (max:4294967295) for anything content-heavy, or at minimum MEDIUMTEXT.

Two Froala fields on one form. Nova keys components by attribute name. Duplicate Froala::make('Body') entries (e.g., on create and update via ->onlyOnForms() tricks) can collide; give each a distinct attribute or use ->onlyOnForms() deliberately.

Debugging checklist, in order.

  1. Blank editor → assets. Console shows FroalaEditor is not defined or a 404 on /nova-vendor/....
  2. Toolbar missing buttons → Froala version vs. option name; check the bundled version first.
  3. Content disappears on save → column too short, or a mutator is stripping tags.
  4. Uploads 419/403 → CSRF route or disk visibility/storage:link.
  5. Editor works but value doesn't persist → field is ->readonly() or hidden by onlyOnDetail.

Deployment. If you publish any config from this package, add php artisan config:clear (or a fresh config:cache) to your deploy script; a stale cached config after an upgrade is an invisible failure, not an exception.

Maintenance risk is real. The package has a single maintainer and a 2023 last release. Pin it in composer.json with a concrete constraint (avoid bare ^), and be prepared to fork and rebuild dist/js/field.js yourself if a Nova major bump or a Froala CVE forces an upgrade. The field class is small enough to vendor-in and maintain in-house if that day comes.

Extension points worth knowing. component() lets you swap in your own Vue component if you need a customized toolbar UI. resolveUsing() / displayUsing() are inherited from Nova's Field and are the correct place to transform stored HTML for index/detail views. And because Froala extends Nova's Field rather than wrapping it, every Nova field modifier (help, rules, sortable, nullable) works unchanged.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib