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

idez/nova-checkboxes-field

A Laravel Nova field package that adds a checkbox group input to resource forms, letting users select and store multiple options at once. Drop-in checkboxes field with native Nova styling and minimal setup for multi-select values.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Audit compatibility before you write a line of feature code. The last release was June 2022, so this field predates Nova 4.30+ and all of Nova 5. It ships a prebuilt dist/js/field.js compiled against whatever Vue version was current then; a Vue 2 bundle installs cleanly into a Vue 3 Nova and then fails at runtime with a blank cell or Failed to mount component. Check before anything else:

composer require idez/nova-checkboxes-field
cat vendor/idez/nova-checkboxes-field/composer.json   # laravel/nova constraint
cat vendor/idez/nova-checkboxes-field/package.json    # vue version
grep -r "class .* extends Field" vendor/idez/nova-checkboxes-field/src

The grep gives you the real namespace and class name — the README is the only other place it's documented, and with 5 stars and 0 dependents you should assume nothing is verified by anyone but you.

2. Read the Vue components before writing a migration. This is the single highest-leverage ten minutes you will spend on the package:

  • resources/js/components/FormField.vue — what this.field.value is initialized as, and exactly what this.$emit('input', …) emits. Array? JSON string? Nested object in grouped mode?
  • IndexField.vue / DetailField.vue — how null renders (does it call .join() unguarded?) and whether grouped values get flattened for display.

Everything downstream — column type, cast, validation rule, filter, accessor — depends on the answer. Decide once, write it in a comment on the migration, move on.

3. Migration and cast (adjust to what you found in step 2; the flat case is almost certainly a JSON array):

Schema::table('users', fn (Blueprint $t) => $t->json('channels')->nullable());
protected $casts = ['channels' => 'array'];

4. First use case — a multi-select "notification channels" field, which is the canonical fit: a handful of static options, stacked checklist affordance, no relation to maintain:

use Idez\NovaCheckboxes\Checkboxes; // confirm in step 1

Checkboxes::make('Channels')
    ->options([
        'email' => 'Email',
        'sms'   => 'SMS',
        'push'  => 'Push',
    ])
    ->rules('nullable', 'array'),

If the component doesn't render at all, republish assets — there is no config file, so nothing else to debug here:

php artisan vendor:publish --provider="Idez\NovaCheckboxes\FieldServiceProvider" --force
php artisan nova:publish

Implementation Patterns

Flat options: mirror the Select API

->options() takes the same [value => label] array as Nova's built-in Select, so anything pluck()-able works directly. Keys are what get stored:

Checkboxes::make('Roles')
    ->options(Role::orderBy('name')->pluck('name', 'id'))

Grouped checkboxes (1.1.0 headline feature)

The release adds grouped rendering — one field, several labelled checkbox blocks. It is effectively a permission-grid UI:

Checkboxes::make('Permissions')
    ->groups([                          // confirm method name in the README
        'Content' => [
            'posts.create' => 'Create posts',
            'posts.delete' => 'Delete posts',
        ],
        'Users' => [
            'users.invite' => 'Invite users',
            'users.ban'    => 'Ban users',
        ],
    ])

Before building on it, verify two things — from the README example, FormField.vue, and the network tab on save:

  1. The exact method name and accepted array shape.
  2. Whether the persisted value is a flat array of leaf keys (['posts.create','users.ban']) or a nested array mirroring the groups (['Content' => ['posts.create']]).

It is almost certainly flat. But validation, filters, whereJsonContains, display formatting, and every diff you ever run against it depend on that answer. Settle it on day one.

Build the option set once, share it

Grouped mode doubles the drift risk: the array fed to groups() and the allow-list used for validation must stay identical or users get rejected for checking a box you rendered. Put it behind a static:

// app/Nova/Fields/PermissionCheckboxes.php
public static function groups(): array { /* single source of truth */ }

public static function make(string $attribute = 'Permissions'): Checkboxes
{
    return Checkboxes::make($attribute)->groups(static::groups());
}

Then flatten the same source for the rule: array_keys(Arr::dot(self::groups())).

Validation happens at the attribute, not the element

Nova applies field rules to the attribute, so 'channels.*' won't attach to the field itself. Two workable routes:

// a) whole-attribute rule
->rules(['array', new AllowedChannels])

// b) element rules in NovaRequest / FormRequest
'channels.*' => [Rule::in(array_keys(Channel::options()))],

Do not rely on groups() being enforced server-side. A crafted /nova-api payload can submit arbitrary keys — including your group labels, since nothing in the field stops it.

Normalize on the way in and out

If FormField.vue emits a JSON string instead of an array, convert once at the field rather than patching vendor code:

->fillUsing(function ($request, $model, $attribute) {
    $value = $request->input($attribute);
    $model->{$attribute} = is_string($value) ? json_decode($value, true) : $value;
})
->resolveUsing(fn ($value) => $value ?? [])

resolveUsing returning [] rather than null avoids the create-form render loop choking on a missing value — grouped mode has more moving parts in that loop and is likelier to trip.

Index/detail display

The built-in rendering is plain and grouped values have no obvious single-line form. Override it, mapping against a flattened label lookup (Arr::dot() of the groups array) when grouped:

->displayUsing(fn ($value) => collect($value ?? [])
    ->map(fn ($k) => $labels[$k] ?? $k)
    ->implode(', '))

Or ->hideFromIndex() and add a badge/text column — often nicer for a permission set anyway.

Filters

Straightforward on MySQL 5.7+ / Postgres, provided the stored value is a flat array:

$query->whereJsonContains('channels', $value);

SQLite (your test env) has no whereJsonContains; use where('channels','like',"%\"{$value}\"%") or normalize to a pivot table. Note whereJsonContains('channels', 1) will not match a stored "1" — see Gotchas.

Relations

The field maps to one column, not a pivot. You can bridge to belongsToMany with fillUsing (sync) + resolveUsing (pluck), but at that point a native BelongsToMany field with ->multiple() is less code and no unmaintained dependency.

Test the round-trip, since there is no test suite

No tests ship with the package. One feature test against the Nova API catches most failure modes, and is the fastest way to discover what grouped mode actually persists:

$this->putJson("/nova-api/users/{$user->id}", ['permissions' => ['posts.create','users.ban']])
     ->assertOk();

$this->assertSame(
    ['posts.create','users.ban'],
    collect($user->fresh()->permissions)->sort()->values()->all()
);

Assert the exact array, not just assertOk() — that's what catches group labels and nested shapes leaking into storage.

Gotchas and Tips

  • Version drift is the primary risk, not a footnote. 1.1.0 is a feature release (grouped checkboxes + README examples), not a compatibility rebuild. The grouped mode added fresh Vue template code — eyeball it for Vue 2 syntax before trusting it in a Vue 3 Nova. If checkboxes render but never persist, suspect the base-class/props contract, not your migration.
  • Confirm the grouped value shape before writing anything downstream. Flat leaf-key array vs. nested object is the difference between a one-line whereJsonContains and a day of refactoring filters, accessors, and diff logic.
  • Group labels can leak into stored values. A naive v-model on a nested object happily emits group keys alongside leaf keys, so you silently persist 'Content' next to 'posts.create'. Your exact-array assertion catches this; assertOk() does not.
  • Duplicate leaf keys across groups. Nothing enforces uniqueness. Storage dedupes, label lookup picks whichever group iterated last, and your allow-list quietly disagrees with the UI. Keep leaf keys globally unique and assert it in the builder.
  • Empty selection is the classic bug. Unchecking everything often submits null or '' instead of [] — fatal on a NOT NULL JSON column or in an accessor foreach. Grouped mode raises the odds of a sparse/nested payload when one whole group is unchecked. Guard both the fill and the resolve side.
  • Integer keys become strings. Keys 1,2,3 round-trip through JSON and the browser as "1","2". whereJsonContains('channels', 1) won't match. Pick string keys throughout; in_array((string) $id, $value, true).
  • searchable() is a trap on JSON columns. Nova generates a LIKE against the blob — false positives across keys, worse now that group labels live in the blob too. Add a model scope using whereJsonContains, or denormalize a channels_text column for the index column to search.
  • Order is not guaranteed. Values arrive in DOM order, which changes with click sequence; grouped mode adds a group-major ordering you don't control. sort($value) before comparing, diffing ("did permissions change?"), or storing.
  • Null-safe index rendering. IndexField.vue may call .join() on null; pre-existing records then show a broken cell. Backfill or override with displayUsing. The grouped index path is newer and less exercised — check it explicitly rather than assuming it behaves like the flat one.
  • Debug the payload, don't guess. Network tab on /nova-api/{resource}/{id} → inspect the key in the response → match your cast. Server side: ->fillUsing(function () { logger(request()->all()); }) shows exactly what Nova receives.
  • No config file, no extension points. One field class, no service-provider config, events, or contracts — 1.1.0 added a rendering mode, not an extensibility layer. Still no "select all", max-selection limit, or ordering hook. If you need behaviour it doesn't offer, fork: copy src/ + resources/js/ into nova-components/checkboxes/, rebuild, depend on the local path. MIT makes this legitimate, and given the release cadence it is the realistic long-term plan — budget the fork when you adopt, not when you're surprised.
  • Prefer built-ins where they fit. BooleanGroup (stores {"key": true}, renders toggle pills) and Select::make()->multiple()->options(...) cover most checkbox needs at zero maintenance. Grouped mode narrows the gap — BooleanGroup can't group — but if your "groups" are really just categories, a flat field with sorted options gets you most of the way. Choose this package when you specifically want the stacked, grouped checkbox-list UI and are willing to own the fork.
  • Authorization is yours. Beyond Nova's base Field there is no canSee/canUpdate logic. Gate sensitive option sets in the resource's fields() with a policy check — that matters more now that a single field can render a whole permission matrix.
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