optimistdigital/nova-multiselect-field
A searchable multiselect field for Laravel Nova with tag support, relationship-friendly options, and a clean Vue-powered UI. Add multi-value selects to resources and forms while keeping Nova’s native look and feel, with flexible configuration for options and display.
Install the package with Composer (note that the Composer name and PHP namespace differ — see Gotchas):
composer require optimistdigital/nova-multiselect-field
Nova 4/5 auto-registers the field's assets through the package's service provider; no vendor:publish or manual Nova::script() call is normally needed. Just make sure you're on the major that matches your Nova version (^4.x for Nova 4, older tags for Nova 3).
First use case — a tags column on a Post model:
use Outl1ne\MultiselectField\Multiselect;
public function fields(NovaRequest $request)
{
return [
Multiselect::make('Tags', 'tags')
->options([
'laravel' => 'Laravel',
'vue' => 'Vue',
'nova' => 'Nova',
])
->saveAsJSON()
->searchable()
->rules('array'),
];
}
Add the cast on the model when using saveAsJSON():
protected $casts = ['tags' => 'array'];
Two things to decide immediately, because they're hard to change later: storage format (comma-separated string is the default; saveAsJSON() is almost always what you want) and whether the column is text/json. If in doubt, use JSON — it survives values containing commas, and it round-trips cleanly against the array cast.
Static options. The simplest form is a key => label array. Keys become the stored values, labels are what the user sees. Stick to scalars — strings and ints are safe; objects and arrays are not.
Dynamic / dependent options. options() accepts a closure, which is evaluated server-side each time the field resolves:
Multiselect::make('Subcategories')
->options(fn () => Category::where('parent_id', $this->parent_id)->pluck('name', 'id'))
->saveAsJSON()
->dependsOn('parent_id'), // Nova reloads the field when parent_id changes
This is the idiomatic pattern for cascading selects. Keep the closure cheap — it runs on every resolve, including index rows if the field is visible there.
Large lists. Add ->searchable() (backed by vue-multiselect) and ->max(n) to cap selections. ->placeholder('Pick tags…') is worth setting for discoverability, especially on empty nullable fields.
Validation. The request payload arrives as an array, so pair it with ->rules('array') and validate the values, not the labels:
->rules('array')
->rules(fn () => ['array', Rule::in(array_keys($this->allOptions))])
->nullable() plus array rules avoids "must be an array" errors on optional fields.
Display. On index/detail views the raw stored value is shown unless you customize it. Use Nova's displayUsing() to map keys to labels for a readable index column, and hideFromIndex() if the column is noisy. Because the stored value is often IDs, a naive index shows 3,17,42 — worth fixing early.
Multiple fields, one resource. Fields are independent; there is no shared state. If you need pivot-backed many-to-many, use Nova's BelongsToMany/MorphToMany instead — this field is for a single denormalized column, not relations.
optimistdigital/nova-multiselect-field, the PHP namespace is Outl1ne\MultiselectField, and the GitHub org is outl1ne. Don't "correct" the import to OptimistDigital\... — it will fail.saveAsJSON() unless you have a strong reason not to. If you must use strings, saveAsString() accepts an alternate separator in recent versions — verify the signature for your pinned release.saveAsJSON() + the array cast, the package handles encoding. If you also json_encode in a mutator or accessor, you'll get "[\"a\",\"b\"]" stored as a string and a broken field on edit. Pick one layer.pluck('label', 'id') or an associative array. Returning a list of Eloquent models produces labels like {"id":1,...} and a confusing UI.->onlyOnForms() or hideFromIndex(), or memoize the query result.[], null, or be absent depending on Nova version. If you see stale values on save, confirm your fillAttributeFromRequest path is receiving the key — log $request->all() in a temporary resolve/fill override.dd($field->json, $field->options) in a throwaway route, or inspect the fields payload in the Nova resource response in the browser network tab. Most "options not showing" issues are shape or visibility problems, not JS.composer.json and check the changelog when bumping Nova — the asset build (Nova 3 webpack vs Nova 4 Vite) changed across majors and a mismatch results in a field that renders as an empty div.resolveUsing() / displayUsing() or a subclass overriding options(), rather than patching vendor files. The package ships no views worth publishing for typical use.How can I help you explore Laravel packages today?