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

fourstacks/nova-checkboxes

fourstacks/nova-checkboxes adds a multiple-checkbox field to Laravel Nova, letting admins select several options for one attribute. Define option lists in PHP, render them as checkboxes, and save selected values as an array—useful for settings, tags, permissions, and multi-select forms.

View on GitHub
Deep Wiki
Context7

Getting Started

fourstacks/nova-checkboxes gives you a Checkboxes Nova field: a group of native checkboxes in a resource form, whose selected keys are persisted as a JSON array on the model. Reach for it when a record needs a small, fixed set of independent flags — notification channels, feature entitlements, permissions, content categories — and a multi-select dropdown would hide the options behind a click.

Before you run composer require, check the Nova constraint. The package is archived, its last release was December 2019, and it targets Nova 2.x. Open the repo's composer.json and compare the laravel/nova requirement against what your app actually runs. If you are on Nova 4+, stop here and use the native BooleanGroup field instead — same JSON array storage shape, zero migration of existing data:

use Laravel\Nova\Fields\BooleanGroup;

BooleanGroup::make('Features')->options(Features::ALL),

If you are on Nova 2/3 and want the field anyway, install it:

composer require fourstacks/nova-checkboxes

There is no vendor:publish step — the service provider is auto-discovered and registers the compiled asset via Nova::script(). If the field renders as an empty table cell later, that registration is the first thing to check.

Database and cast, done before the field. Both of these are mandatory, and forgetting either is the single most common reason the field "doesn't work":

// migration
$table->json('features')->nullable();

// model
protected $casts = ['features' => 'array'];

Without the json/text column and the array cast, saving throws Array to string conversion, or the field reloads unchecked despite a successful-looking submit.

Minimal field definition. Add it to fields():

use Fourstacks\NovaCheckboxes\Checkboxes;

public function fields(Request $request)
{
    return [
        Checkboxes::make('Features')->options([
            'newsletter'      => 'Newsletter',
            'product_updates' => 'Product updates',
            'beta_access'     => 'Beta access',
        ]),
    ];
}

Use associative arrays with stable string slugs. A plain list (['Newsletter', 'Updates']) stores integers 0,1, and inserting or reordering an option silently reinterprets every existing record. This is not negotiable.

Where to look first when something breaks. Read src/Checkboxes.php for the field class and options()/resolve logic, then resources/js/components/FormField.vue and IndexField.vue to see exactly how the value is shaped on the wire. dist/js/field.js is a committed build — if you fork, you must rebuild it.

First day-to-day use case. A "notification channels" field on a User resource. Operators tick the boxes, the detail view renders the labels, and your queued job branches on the stored array:

$channels = (array) $user->features;
if (in_array('sms', $channels, true)) {
    // send SMS
}

Implementation Patterns

Own the option list in one class. Labels are needed in the form, in validation rules, in filters, in Blade views, and in jobs. Define them once so they can never drift:

// app/Support/Features.php
final class Features
{
    public const ALL = [
        'newsletter'      => 'Newsletter',
        'product_updates' => 'Product updates',
        'beta_access'     => 'Beta access',
    ];

    public static function keys(): array
    {
        return array_keys(self::ALL);
    }
}

Defaults and validation on the same field.

Checkboxes::make('Features')
    ->options(Features::ALL)
    ->default(['newsletter'])
    ->rules('array')
    ->rules(['features.*' => 'in:'.implode(',', Features::keys())]);

Note the wildcard rule has to be passed as its own array entry in a separate rules() call — that is how Nova forwards per-element rules.

A reusable field factory. If the same group appears on several resources, wrap it so options and validation stay in lockstep:

public static function features(): Checkboxes
{
    return Checkboxes::make('Features')
        ->options(Features::ALL)
        ->rules('array');
}

Index and detail presentation. The shipped index/detail components render the selected labels and no more. When you want badges, counts, or a different separator, use a computed text field rather than fighting the Vue component:

Text::make('Features', function () {
    return collect($this->features ?? [])
        ->map(fn ($k) => Features::ALL[$k] ?? $k)
        ->implode(', ');
})->onlyOnIndex(),

Keep the ?? $k fallback — rows written before an option was renamed will otherwise vanish from the display.

Filtering a JSON column. No filter ships with the package, so you write one. Use JSON operators, never LIKE, or beta will match beta_access:

public function apply(Request $request, $query, $value)
{
    return $query->whereJsonContains('features', $value);
}

public function options(Request $request)
{
    return Features::keys();
}

whereJsonContains works on MySQL 5.7+/MariaDB 10.2+ and Postgres jsonb. On SQLite fall back to json_each, or LIKE '%"key"%' — including the quotes, which is what keeps it from being a substring match.

Conditional fields. Nova's dependsOn hands the dependent field the whole array, not a boolean. Normalise inside the callback rather than trying to compare arrays in Vue:

Text::make('SMS Number')->resolveUsing(function ($value) {
    return $this->resource->sms_number;
})->dependsOn(['features']),

Bulk actions and jobs. Treat the attribute as a plain PHP array and guard against null from rows created before the field existed:

$features = $action->models->pluck('features')->map(fn ($f) => (array) $f);
$betaUsers = $features->filter(fn ($f) => in_array('beta_access', $f, true));

Seeders and factories. Write the JSON array directly; there is nothing Nova-specific about persistence:

User::factory()->create(['features' => ['newsletter', 'beta_access']]);

Gotchas and Tips

  • Archived means you own it. No fixes, no issue triage, no Nova 4/5 compatibility work. The pragmatic day-to-day stance: treat the source as a reference implementation, and if you need it long term, vendor the field class and .vue component into your own codebase under MIT rather than pinning an abandoned Composer dependency inside your authenticated admin surface.
  • Nova version mismatch is the top failure mode. The compiled Vue component predates modern Nova asset pipelines and the Vue 2 → 3 shift. On Nova 3+ expect a blank field or a browser console error, not a PHP exception — which makes it easy to misdiagnose as a backend problem. Check the console first, then the network tab for field.js.
  • Missing $casts beats every other bug report. Without 'array' on the model, saves fail or the field re-renders unchecked. Verify the cast before you read a single line of JavaScript.
  • Stable string keys, always. Integer keys from a plain list mean reordering options rewrites the meaning of your data. If you inherit a field built with a list, migrate the stored values to slugs before touching the options.
  • Empty state is inconsistent. Depending on Nova version and column nullability you can get null, [], or '' back. Normalise with (array) $model->features at every read site, and use strict in_array(..., true).
  • Labels aren't searchable. Nova's index search won't traverse the JSON for label text. Add a scopeSearch or a generated column if operators need to find records by ticked option.
  • JSON filtering is a full scan unless indexed. whereJsonContains on a large table has no index to use. Add a functional/generated column index (MySQL 8) or a GIN index on jsonb (Postgres) before the table grows, not after.
  • Asset caching after any fork. Rebuild dist/js/field.js, run php artisan nova:publish, and hard-refresh. A stale field.js in the browser looks identical to a broken field, and you will burn an hour on it.
  • Extension points are thin. The field class is small. Per-option disabled states, grouping, or "select all" require forking the Vue component — you cannot bend options() into doing it. Budget accordingly, and remember the fork is yours to maintain.
  • Migrating off is cheap, so keep the door open. Nova 4's BooleanGroup stores the same JSON array shape. Switching is BooleanGroup::make('Features')->options(Features::ALL) plus dropping the package, with no data transformation. Build your option constants and read sites so that swap is a one-file change.
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