marshmallow/nova-multiselect-field
A Laravel Nova field that lets you attach multiple selectable options to a resource in one clean input, with searchable choices and a simple API for defining options and storing selected values as arrays.
This is a single-field Nova package: it provides one custom field class (a multi-value select) plus a compiled Vue component. Treat it as a small dependency, not a framework — read the source before you rely on it. With 0 stars, 0 dependents, and a single maintainer, you should plan to pin the version and keep a fork within reach.
Install and inspect
composer require marshmallow/nova-multiselect-field
Then look at, in order:
vendor/marshmallow/nova-multiselect-field/composer.json — confirm the laravel/nova constraint and PHP minimum against your app. This is the number one reason these packages silently break.vendor/marshmallow/nova-multiselect-field/src/ — locate the field class (typically Marshmallow\NovaMultiselectField\Multiselect) and check whether it extends Field or Select. That tells you whether ->options(), ->displayUsing(), and ->rules() behave like a normal Nova select.vendor/marshmallow/nova-multiselect-field/dist/js/ — if a compiled field.js exists, no npm run dev step is needed. If it's absent, you'll have to build the package's frontend yourself, which is a red flag worth catching on day one.README.md for the documented ->options() / ->multiple() signature — the method names are the contract, and you should not guess them.First use case: replacing a comma-separated tags column or a checkbox group. Create a JSON column first:
// migration
$table->json('regions')->nullable();
// app/Models/Project.php
protected $casts = ['regions' => 'array'];
// app/Nova/Project.php
use Marshmallow\NovaMultiselectField\Multiselect;
Multiselect::make('Regions')
->options([
'emea' => 'EMEA',
'apac' => 'APAC',
'amer' => 'Americas',
])
->rules('array')
->help('Select every region this project ships to.'),
Smoke-test by saving two values, reloading the detail view, and inspecting the raw column (select regions from projects). You want ["emea","apac"] — not "emea,apac" and not "[\\"emea\\"]". Confirm which shape the field writes before building anything on top of it.
Static option sets are the sweet spot. Regions, tags, permissions, feature flags, notification channels. If the options live in a database table with their own attributes, use a BelongsToMany relationship field instead — a multiselect storing IDs you can't join against will hurt when you need ordering, counts, or admin CRUD for the options.
Options from a model or enum. If the field accepts a callable, this is the cleanest way to keep a single source of truth:
use App\Enums\Region;
Multiselect::make('Regions')
->options(fn () => collect(Region::cases())
->mapWithKeys(fn ($c) => [$c->value => $c->label])
->all());
If it only accepts an array, build that array in a small Options helper class so the same list feeds Nova, validation, and the filter.
Validation. Nova applies rules to the resolved value, so array rules are mandatory:
->rules('array')
->rules('min:1')
->rules(fn () => Rule::in(array_column(Region::cases(), 'value')))
Without ->rules('array'), a in: rule can blow up with "Array to string conversion" on submit.
Keep the model cast and the field in sync. The single most common failure mode is a field writing JSON while the model has no array/AsArrayObject cast (or vice versa: a comma-separated column with an array cast). Add a feature test that saves through the Nova endpoint and asserts the stored value.
Index and detail display. JSON arrays don't render nicely by default:
->displayUsing(fn ($value) => collect($value ?? [])
->map(fn ($v) => Region::from($v)->label)
->join(', '))
Filtering. You'll need a custom Nova filter; there's no built-in search for JSON columns:
public function apply(Request $request, $query, $value)
{
return $query->whereJsonContains('regions', $value); // MySQL 5.7+, Postgres jsonb
}
For Postgres json (not jsonb), or SQLite in local dev, whereJsonContains behaves differently or isn't available — write the filter defensively and test against your real database engine.
Conditional options. If the field supports Nova's ->dependsOn(), use it for dependent selects (country → region). If it doesn't, the options won't recompute on the client and you'll need to render the field only when the parent is set, or drop to two separate fields in a Flexible/Repeater.
Escape hatch: subclass it. Rather than patching vendor/, extend the field and override resolve() / fill() to normalize nulls and empty arrays to a shape the rest of your app understands:
class Regions extends Multiselect
{
public function resolve($resource, $attribute = null)
{
parent::resolve($resource, $attribute);
$this->value = $this->value ?? [];
}
}
composer.json constraint and the compiled field.js before upgrading Nova — a 0-dependency field can't be assumed to keep pace with Nova majors.composer.json "repositories" fork entry ready, and pin marshmallow/nova-multiselect-field to an exact version in composer.json rather than a caret range.null, [], or '' and enforce it in a mutator. Mixed empties are a recurring source of implode() warnings in resources and false positives in whereJsonContains.php artisan nova:publish and hard-refresh; if dist/js/field.js doesn't exist in the vendor directory, you must build it.PUT /nova-api/... body. That instantly tells you whether the component is sending ["emea","apac"], a single string, or a nested object — far faster than adding dd() to the field.options(), rules(), displayUsing(), and whatever Select/Field inherits. Don't assume dependsOn, searchable, or sortable work — verify in src/ before designing a form around them.How can I help you explore Laravel packages today?