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

outl1ne/nova-multiselect-field

Laravel Nova multiselect field with searchable multi- and single-select, async resource querying, drag-and-drop reordering, option dependencies, distinct value groups, and on-the-fly tagging. Stores values as JSON and supports light and dark modes.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and confirm your Nova version. The current release targets Laravel Nova 5 and PHP 8.1+. If you're still on Nova 4 or 3, use the version branches (nova-v4, nova-v3) rather than the default — pinning the Composer constraint correctly up front saves a broken admin panel later.

composer require outl1ne/nova-multiselect-field

Decide your storage column first. This is the single biggest day-one decision. By default values are persisted as a stringified JSON array ('["liverpool","tottenham"]'), so the DB column should be string, text, or varchar. If you're using a native JSON column, chain ->saveAsJSON(). Getting this wrong is the most common cause of "the field saves but won't load" tickets.

Where to look first: the README's options table. Almost every capability in the package (max, reorderable, taggable, distinct, singleSelect, nullable, index display controls) is a single fluent method documented there. Skim that table before writing custom JS or reaching for a different field type.

First use case — a simple static-select field:

use Outl1ne\MultiselectField\Multiselect;

public function fields(Request $request)
{
    return [
        Multiselect::make('Football teams', 'football_teams')
            ->options([
                'liverpool'   => 'Liverpool FC',
                'tottenham'   => 'Tottenham Hotspur',
            ])
            ->placeholder('Choose football teams')
            ->max(4),
    ];
}

The API mirrors Nova's native Select field, so if you've built a Select before, you already know this one. The difference is that the returned value is an array of keys, and it can support search, async loading, and reordering out of the box.

Implementation Patterns

Static options with groups. Use Laravel's familiar option-group syntax; the package reads the group key and renders grouped dropdowns. Add ->groupSelect() when users should be able to pick an entire group in one click — handy for permission pickers where "all admin perms" is a common action.

->options([
    'MS' => ['label' => 'Small', 'group' => 'Men Sizes'],
    'WS' => ['label' => 'Small', 'group' => 'Women Sizes'],
])
->groupSelect()

Async model querying. For large datasets, don't load everything into the page. asyncResource(Artist::class) wires up a Resource-backed search endpoint for you; reach for ->api('/api/...', Artist::class) only when you need custom filtering logic outside the Resource's search. Both accept the user's input via the search query parameter and expect a plain key-value map back.

BelongsToMany relationships. This is the pattern that replaces a third-party pivot UI in most projects:

public function categories()
{
    return $this->belongsToMany(Category::class);
}

Multiselect::make('Categories', 'categories')
    ->belongsToMany(Category::class),          // searchable, loads on type
    // ->belongsToMany(Category::class, false) // preloads everything on page load

Default to the searchable form unless the related table is guaranteed small; the README explicitly warns against preloading for unbounded row counts.

Dependent dropdowns. optionsDependOn('country', [...]) gives you cascading selects without any frontend code. Combine with ->optionsDependOnMax([...]) when the cap varies by parent — e.g. Italy allows one language, Singapore allows three. Note that the parent field's key is what drives the mapping, so keep your field attribute names stable.

Distinct groups. When you have several multiselects that must not share values (e.g. primary/secondary/tertiary category pickers), assign them the same ->distinct('categories') group. The frontend syncs selected values across the fields and disables already-used options — no custom JS needed.

Reorderable + taggable. ->reorderable() preserves the array order as the user drags, which lets you store user-defined sort orders directly. ->taggable() permits free-text values outside the options list, useful for flexible taxonomies where you want a controlled vocabulary but need an escape hatch. Pair ->clearOnSelect() with taggable to keep the input clean.

Display tuning for the index table. indexDelimiter, indexValueDisplayLimit, and indexCharDisplayLimit control how arrays render in the resource list. Tune these early — a multiselect with 50 tags will otherwise blow out the index row.

Reading values in the model. Since by default you get a JSON string, cast it on the model so the rest of your app deals in arrays:

protected $casts = [
    'football_teams' => 'array', // for JSON columns (with saveAsJSON)
];
// or an accessor for the stringified default:
public function getFootballTeamsAttribute($v) { return json_decode($v, true) ?? []; }

Cosmetic overrides. If you need to change how a value looks on detail or how a tag looks in the form, register nova-multiselect-detail-field-value or form-multiselect-field-tag as a Vue component in app.js. The detail component receives field and values (array of labels); the tag component receives option and remove. This is the intended extension point — avoid forking the package.

Gotchas and Tips

  • Stringified JSON vs native JSON. If the column is json but you forget ->saveAsJSON(), you'll get double-encoded data. If the column is varchar but you add ->saveAsJSON(), you'll get invalid storage on strict databases. Match the column and the flag every time.
  • Empty vs null. With ->nullable() an empty selection stores null; without it, empty selections store []. Pick the behavior intentionally and mirror it in validation rules and accessors.
  • singleSelect() changes the return type. Unlike the multiselect mode, it returns a scalar rather than an array — remember to adjust accessors, in: validation rules, and any Blade templates that expect foreach.
  • Async requires a search term by default. Users won't see any options until they type. If they complain, either switch to belongsToMany($resource, false) for small sets or make sure your Resource's searchable columns are configured — asyncResource relies on them.
  • Custom resource keys. If your Resource uses a non-id key, use the resourceKeyName() helper (added in 4.2) so returned option keys match what your model expects on save.
  • distinct is per-request UI state. It disables options already used by sibling fields on the same form — don't rely on it as a server-side uniqueness guarantee. Enforce that in validation.
  • optionsDependOn and readOnly. There have been fixes around readOnly not reacting to dependency changes; if you're on an older 4.x, upgrade to pick up the corrected reactivity.
  • Async performance. Set optionsLimit thoughtfully (default 1000). Loading 10 000 options into the DOM will freeze the form even before search kicks in.
  • Namespace migration. If you're coming from an old project, note that OptimistDigital\MultiselectField\Multiselect was renamed to Outl1ne\MultiselectField\Multiselect in 4.0 — update use statements and composer.json together.
  • Localization. If labels or empty-state strings need translating, publish the translations with php artisan vendor:publish --provider="Outl1ne\MultiselectField\FieldServiceProvider" --tag="translations" rather than editing vendor files.
  • Debugging saved values. When in doubt, dd($model->getRawOriginal('your_field')) — it shows exactly what's in the DB, which immediately clarifies whether the problem is the save format (saveAsJSON) or the read format (missing cast).
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.
roundly-consulting/kubernetes-api-for-laravel
roundly-consulting/plausible-for-laravel
gvp/gvp-laravel-template
metasyncsite/laravel-metasync-client
roundly-consulting/onboarding-for-laravel
andydefer/laravel-notification
adachsoft/cli-process-tester
jhaskell13/laraflow
sebastienheyd/boilerplate-media-manager
ccast/tagixo-primix
pretendtrue/laravel-builder
sawmainek/laravel-scout-tntsearch-driver
tomshaw/laravel-calendar-table
dibakar/laravel-ownership
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo