cocosmos/filament-quick-add-select
Add a quickAdd() method to any Filament Select with a relationship so users can instantly create and select new options right from the search dropdown. No modals, no interruptions—just type, click "+ Add", and keep working. Supports multiple selects and custom labels.
Install and confirm compatibility first. The package requires PHP 8.2+, Laravel 11+, and Filament 4 or 5. Check those before composer require cocosmos/filament-quick-add-select — the constraint is ^4.0 || ^5.0 for Filament and ^11.0 || ^12.0 for the Laravel contracts, so an older Filament 3 panel will not resolve.
There is no config file, no migration, and no publish step required. The service provider is auto-discovered and registers a single macro on Filament\Forms\Components\Select. That means the entire feature surface is one method call, which is also why the fastest way to understand the package is to read the provider and the macro body rather than the README — the README covers usage but not internals.
Where to look first:
src/FilamentQuickAddServiceProvider.php — macro registration and translation loading.quickAdd() macro itself — this is what decides when the synthetic "+ Add 'term'" option is injected into search results.CHANGELOG.md — specifically the 1.1.2 whereLike fix for PostgreSQL; it tells you the package runs its own existence query against the related model, which matters for the gotchas below.First use case. Take the most common one: a Post belongs to a Profession, and editors keep hitting terms that don't exist yet.
Select::make('profession_id')
->relationship('profession', 'name')
->searchable() // required — no searchable, no add option
->quickAdd()
Verify it end-to-end: type a term with no match, confirm the "+ Add '…'" row appears at the bottom of the results, click it, and then check the database (or php artisan tinker) that the professions row exists with that exact name and that the select shows the persisted label. If the option never appears, the two usual causes are a missing ->searchable() or an existing exact match (the option is suppressed when a record already matches).
Single relationship select (the default shape). Chain order matters for readability: relationship() → searchable() → quickAdd(). Putting quickAdd() before searchable() is harmless but makes the form definition harder to scan for the required searchable().
Multiple / tagging selects. Same call, no changes needed. This is where the feature pays off most, because modals are especially disruptive in multi-value contexts:
Select::make('skills')
->multiple()
->relationship('skills', 'name')
->searchable()
->quickAdd()
Note that for multiple(), the "Add" row is removed from the dropdown after creation but the search text stays (see resetSearch below), which is the behaviour you want when entering five tags in a row.
Custom labels for house style or brand voice. Both a closure and a template string work; prefer the closure when the label depends on something more than the search term:
->quickAdd(label: fn (string $search) => "Créer : {$search}")
->quickAdd(label: 'New category: {search}')
The closure receives the raw search string, so if you interpolate it into HTML or a tooltip, escape it yourself.
Reset search only when the workflow is finished. Default behaviour preserves the search term so the user can keep adding siblings. Flip it when a single addition ends the interaction:
->quickAdd(resetSearch: true)
A reasonable convention: resetSearch: true on single selects (context is over once the record is chosen), leave the default on multiple().
Gate the affordance by policy, then enforce it again server-side. The enabled argument is a UI switch only — it hides the option, it does not stop the macro from creating records if the payload is crafted. Use it for UX, and keep real authorization in the model policy plus a creating guard if the data is sensitive:
->quickAdd(enabled: auth()->user()->can('create', Category::class))
Combine with createOptionForm() for "fast path + full path". The documented limitation is that quick add only fills the relationship's title attribute. On models with required extra fields, keep the modal for completeness and let quick add handle the 80% case by giving the model sensible defaults in booted() or a column default:
Select::make('category_id')
->relationship('category', 'name')
->searchable()
->createOptionForm([/* full form for edge cases */])
->quickAdd()
This gives editors a one-click path and an escape hatch in the same component — the strongest argument for adopting the package in an existing panel rather than replacing anything.
Multi-tenant panels. relationship() respects the tenant scope for reading, but the create path is the package's own insert; whether tenant_id is set depends on your model's booted()/global scopes, not on the package. Verify before rollout in a tenanted panel — a leaked cross-tenant row is the worst failure mode this feature can produce.
Reduce boilerplate with your own helper. Since the macro is on Select, a small private factory keeps the call sites consistent:
protected static function quickSelect(string $name, string $relation): Select
{
return Select::make($name)
->relationship($relation, 'name')
->searchable()
->quickAdd(resetSearch: true)
->createOptionForm(/* … */);
}
Integration tips. The component works in resources, custom pages, and inside repeaters/builders because it is a form component macro, not a page-level plugin. It also fires normal Eloquent lifecycle events, so audit logging, slug generation, and search-index updates on the related model all run as usual — useful, and worth confirming with whoever owns those observers.
It only works with relationship selects. An options()-based select has nothing to create a record on. If you have a hard-coded options array and want quick add, you need to promote it to a real model and relationship first.
Only the title column is populated. The new record gets the search term written to the relationship's title attribute (name by default) and nothing else. Any NOT NULL column without a database default will throw a query exception the moment a user clicks the option. Audit the related table's schema before shipping — a nullable slug, a status default, and a position default are the usual suspects.
There is no validation of the search term. Whatever the user typed becomes the value: empty-ish strings, 500-character pastes against a varchar(255), emoji into a utf8 (non-mb4) column. Add a maxLength on the related model's fillable validation or a creating hook with a hard trim/length check.
Uniqueness is not guaranteed. The add option is suppressed on an exact match, but case and accent differences can slip past depending on your collation, and two users can race the same term. If the column is unique, expect an occasional QueryException to surface as a Livewire error; catch it in a creating listener or upsert in a firstOrCreate-style observer.
Authorization is not enforced by the package. enabled: is cosmetic. Anyone who can submit the Livewire payload can trigger creation. Pair it with a policy check server-side.
Pin the version, especially on PostgreSQL. 1.1.2 fixed case-sensitive matching by switching to whereLike. On Postgres, anything below that can fail to find existing records and offer to "create" a duplicate. Require ^1.1.2 (or whatever the current tag is) rather than *.
Translation namespace is quick-add::quick-add.add. If you see the raw key in the UI, the translation files were not loaded — historically the cause of 1.0.3. Publish with:
php artisan vendor:publish --tag=quick-add-translations
and edit in lang/vendor/quick-add/{locale}/quick-add.php. The package ships EN, FR, DE, ES, ID, IT, PT, and AR; use the same file layout for any locale you add. The :term placeholder must survive your custom string, or the label loses the search text.
Debugging "the option doesn't appear": check, in order, (1) ->searchable() is present, (2) the select is built on ->relationship() not options(), (3) an exact match already exists for the term, (4) enabled: evaluates truthy, (5) the locale file resolves. If the option appears but clicking it errors, the exception is almost always a database constraint on the related table (missing column, unique violation, not-null).
Be conservative about relying on undocumented behaviour. The package has a single macro and a small surface, but it is early-stage with very few dependents — the interaction between the add option and features like ->preload(), ->reactive() with afterStateUpdated(), and custom getOptionLabelFromRecordUsing() is not documented. Wrap your own smoke test around the components you actually use (a Livewire test that types a term and asserts the record was created) so an upgrade can't silently break data entry.
Extension points: don't fork the macro. Put defaults and normalisation on the related model (booted() for tenant_id/slug, a creating listener for trimming and length caps, a saving observer for audit), which keeps the upgrade path clean and makes the behaviour identical whether the record arrives via quick add, the modal, or a seeder.
How can I help you explore Laravel packages today?