scouser03/group-checkbox
A Laravel package for creating grouped checkbox inputs, letting you organize related checkboxes into structured sets and handle their submitted values cleanly. Useful for forms that need multiple-selection groups with simple, readable markup and backend handling.
This is a Laravel Nova 4 custom field that renders a grouped set of checkboxes and persists the result as an array on your model. With zero stars and zero dependents, treat it as an unvetted internal utility: read the source before you ship it.
Install
composer require scouser03/group-checkbox
Nova 4 is a hard requirement — the field registers a Vue component through Nova's asset pipeline, so it will not work on Nova 3 without modification.
Where to look first
README.md — confirm the exact field class name and the option-mapping signature. Do not assume method names; the API surface is small but undocumented in the wild.src/ — the Field subclass. Note which methods it overrides (fields(), resolve(), fill(), jsonSerialize()) and, critically, what it does to the value on save.resources/js/ — the Vue component. This tells you what shape the frontend sends back (array of values? object keyed by group?) which is what actually lands in your column.dist/ — whether a compiled JS bundle is committed. If not, you will need npm install && npm run prod in the package directory or a nova:publish step.First use case
The natural fit is a bounded permission matrix — a set of capabilities grouped by domain (e.g. Billing, Users, Reports) where a multi-select would lose the grouping context and a KeyValue field would be overkill:
use Scouser03\GroupCheckbox\GroupCheckbox;
GroupCheckbox::make('Permissions')
->options([
'Billing' => [
'invoices.view' => 'View invoices',
'invoices.refund' => 'Issue refunds',
],
'Users' => [
'users.invite' => 'Invite users',
'users.remove' => 'Remove users',
],
])
->rules(['array']);
You will need a matching cast on the model — see the gotchas below.
Model side: always cast to array
The field writes an array; the column must round-trip it. A json column plus an array cast is the only combination that behaves predictably across index, detail, and form views.
protected $casts = [
'permissions' => 'array',
];
Avoid AsArrayObject here unless you have a reason — it makes Nova's resolve() produce an object that the field's Vue component may not hydrate correctly from.
Reusable option sets
Grouped options are almost always domain data, not view data. Put them on a small class or enum rather than duplicating the array across resources:
class PermissionGroups
{
public static function all(): array
{
return [
'Billing' => [...],
'Users' => [...],
];
}
}
GroupCheckbox::make('Permissions')
->options(PermissionGroups::all())
->help('Changes take effect on next login.');
Index and detail views
Grouped checkboxes are unreadable in a table cell. Restrict the field to forms and render a summary elsewhere:
// Resource fields
GroupCheckbox::make('Permissions')
->onlyOnForms(),
Text::make('Permissions', fn () => count($this->permissions ?? []) . ' granted')
->onlyOnIndex(),
If the package ships a displayUsing() or resolves to an array on detail, you can flatten there instead — verify against the source, as a raw array in a detail view renders as ["a","b"].
Pairing with authorization
This is where the field earns its keep day-to-day: gate the options per record, not just per resource.
GroupCheckbox::make('Permissions')
->options(PermissionGroups::all())
->readonly(fn () => ! auth()->user()->isSuperAdmin())
->rules([
'array',
function ($attribute, $value, $fail) {
$allowed = collect(PermissionGroups::all())->flatten(1)->keys();
if (array_diff($value ?? [], $allowed->all())) {
$fail('Unknown permission submitted.');
}
},
]);
Validate server-side. A checkbox group is a client-controlled array; nothing stops a crafted request from injecting values that were never rendered.
Filter integration
If the field is filterable, check whether the package implements Filterable or expects a NovaFilter. With a JSON column, whereJsonContains is the query shape you want:
->whereJsonContains('permissions', $request->value)
Policy / gate consumption
Because the value is a plain array, the field is pleasantly decoupled from enforcement:
Gate::define('refund-invoice', fn (User $u) => in_array('invoices.refund', $u->permissions ?? [], true));
That is the real day-to-day win: the UI is a checkbox matrix, but authorization remains ordinary Laravel code.
Zero stars means zero safety net. No dependents, no issue history to learn from, and no guarantee of Nova 5 support. Before adopting, check the last commit date, whether composer.json pins laravel/nova with a caret constraint, and whether dist/ is committed. If the maintainer disappears, you are maintaining a Vue component — budget for that.
The save path is the whole risk. The one thing you must verify by hand: what the field does when nothing is checked. Some custom checkbox fields submit null, some submit [], some submit nothing at all — and the third case silently leaves stale permissions on the record during an update. Open the component, check the field.value initialiser and the fill handler, then write a test:
public function test_permissions_can_be_cleared(): void
{
$user = User::factory()->create(['permissions' => ['users.invite']]);
$this->actingAs($admin)
->put("/nova-api/users/{$user->id}", ['permissions' => []])
->assertOk();
$this->assertSame([], $user->fresh()->permissions);
}
Cast mismatch is the most common failure. If the column is text or varchar and the cast is missing, Eloquent stores Array as the string "Array" or throws on save. If the column is json but the cast is missing, you get a JSON string back into the Vue component and nothing is checked on the edit form. Symptom: "checkboxes don't persist." Fix: json column + 'array' cast, every time.
Nova asset compilation. Custom fields need their JS built and published. If the package expects you to compile locally, remember nova:publish alone is not enough — you need the package's own npm run prod. A blank or invisible field in the form is nearly always a missing or stale dist/js/field.js, not a PHP bug. Clear public/vendor/scouser03 and rebuild.
Ordering is not guaranteed to survive. Grouped checkboxes typically serialise in display order, but if the user toggles items the array may arrive in click order. If you compare arrays or hash the selection, normalise first:
$perms = collect($user->permissions)->sort()->values()->all();
Don't fight the Vue component — wrap it. If you need behaviour the package doesn't offer (search, select-all-per-group, disabled individual options), resist patching resources/js directly, since a composer update will overwrite it. Either subclass the field and supply your own component via ->component('your-component') after publishing the original into resources/js/fields/, or fork the repo and pin the VCS path in composer.json.
Extension points worth checking in src/. Whether the field exposes ->options(), ->groups(), ->columns() or similar — the naming is the main unknown. Grep the class for public methods returning $this before writing your resource; the README may lag the code.
Migration safety. Backfilling a new grouped field onto an existing table: default the column to [] and make the field nullable in the form. Otherwise every existing record shows an empty matrix and the first save may write null where downstream gates expect an array — use $user->permissions ?? [] defensively in policies regardless.
How can I help you explore Laravel packages today?