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

Laravel Selectable Laravel Package

ringlesoft/laravel-selectable

Laravel Selectable generates HTML select options directly from Laravel collections using a fluent, chainable API. Customize labels and values with strings or closures, set selected/disabled options, add data attributes, grouping, and export arrays for AJAX or SPAs.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and render your first select

composer require ringlesoft/laravel-selectable

There is no service provider config to publish, no config file, and no migrations. The package adds macros to Laravel's collection so the API is available anywhere a Collection is: controllers, view composers, Blade components, tests, and Tinker.

The fastest first use case is replacing a hand-written @foreach in a Blade form:

<select name="user_id" class="form-select">
    {!! \App\Models\User::all()->toSelectOptions() !!}
</select>

Defaults are id for the value and name for the label, so a standard users table just works. The inline positional signature is toSelectOptions($label, $value, $selected, $disabled):

{!! \App\Models\User::all()->toSelectOptions('email', 'uuid', $user->uuid) !!}

Where to look first

  1. README "Usage" section — the three tiers (inline params, toSelectable() fluent builder, toSelectItems()) cover essentially the whole surface area of the package.
  2. Selectable class in src/ — this is where withLabel, withValue, withSelected, withDisabled, withDataAttribute, withId, withClass, toSelectItems, and toSelectOptions live. Read the two to* methods first; they reveal exactly what is and is not escaped.
  3. The IDE helper file (added in 1.0.2, improved in 1.0.5) — if your editor does not autocomplete toSelectable() on Collection, that helper is where the annotations come from.
  4. CHANGELOG — 1.0.3 added non-object/array collection support, 1.0.4 added withId and closure data attributes. Anything older than 1.0.4 is missing methods referenced in the README.

First real use case

The package pays for itself the moment you have a model whose display name is not name, which is almost immediately:

<select name="assignee_id">
    {!! $project->members()->get()->toSelectOptions(
        label: fn ($u) => "{$u->first_name} {$u->last_name}",
        value: 'id',
        selected: $task->assignee_id
    ) !!}
</select>

That single call replaces the loop, the selected ternary, and the null-check for a missing assignee.


Implementation Patterns

Pattern 1: Build the string in the controller/composer, not the view

The README's inline User::all() examples are illustrative, and the README itself warns against them. In day-to-day code, build options in a controller, a Blade component, or a dedicated view model and pass the finished string down:

// app/Http/Controllers/TaskController.php
public function edit(Task $task)
{
    $assignees = User::query()
        ->where('active', true)
        ->orderBy('first_name')
        ->get()
        ->toSelectOptions('name', 'id', $task->assignee_id);

    return view('tasks.edit', compact('task', 'assignees'));
}
<select name="assignee_id" class="form-select">
    {!! $assignees !!}
</select>

This keeps queries out of templates and makes the options cacheable/testable.

Pattern 2: Wrap it in a Blade component for consistency

Because the package returns a raw HTML string, the cleanest integration is an anonymous Blade component that owns the escaping decision once:

{{-- resources/views/components/select.blade.php --}}
@props(['name', 'options', 'selected' => null, 'multiple' => false, 'placeholder' => null])

<select name="{{ $name }}" @if($multiple) multiple @endif {{ $attributes->merge(['class' => 'form-select']) }}>
    @if($placeholder)
        <option value="">{{ $placeholder }}</option>
    @endif
    {!! $options !!}
</select>

Now every form in the app renders selects identically and a future swap away from the package touches one file.

Pattern 3: Fluent builder for anything non-trivial

Reach for toSelectable() the moment you need two or more customizations:

$options = User::with('team')->get()
    ->toSelectable()
    ->withValue('uuid')
    ->withLabel(fn ($user) => "{$user->name} ({$user->team?->name})")
    ->withSelected($task->assignee_ids ?? [])
    ->withDisabled(fn ($user) => ! $user->active)
    ->withId(fn ($user) => "assignee-{$user->uuid}")
    ->withDataAttribute('team', fn ($user) => $user->team?->slug ?? '')
    ->withClass('form-option')
    ->toSelectOptions();

This is the workhorse pattern for admin panels: UUID values, composite labels, multi-select preselection, inactive users disabled rather than hidden, plus a data-team attribute for a client-side filter.

Pattern 4: JSON for Livewire / Inertia / Alpine

toSelectItems() exists precisely so you do not parse HTML on the frontend:

public function mount()
{
    $this->users = User::all()->toSelectable()->toSelectItems();
}

The shape is stable and easy to consume:

[
    [
        'label' => 'Jane Doe',
        'value' => '2f3a-...',
        'selected' => false,
        'disabled' => false,
        'dataAttributes' => ['hidden' => false],
        'classes' => ['form-option', 'custom'],
    ],
]

Two practical uses: (a) feeding a JS component that renders its own markup, and (b) server-side validation by deriving the allowed set:

$allowed = collect(User::all()->toSelectable()->toSelectItems())->pluck('value');
$request->validate([
    'user_id' => ['required', Rule::in($allowed)],
]);

Pattern 5: Grouped options via groupBy

Grouping is delegated to Laravel's own groupBy, and the package renders <optgroup> from the result:

$options = User::all()
    ->toSelectable()
    ->groupBy(fn ($user) => $user->department)
    ->toSelectOptions();

Call groupBy on the Selectable object, not on the collection before toSelectable(). Grouping first gives you a collection of collections whose items no longer answer to withLabel/withValue accessors, and the labels fall back to whatever the inner collection happens to serialize as.

Pattern 6: Non-object collections

For config arrays, enums mapped to labels, or raw query results:

// flat list, label === value
collect(['Draft', 'Published', 'Archived'])->toSelectOptions();

// associative, key = value, entry = label
collect(['draft' => 'Draft', 'published' => 'Published'])->toSelectOptions();

// array of arrays, pick the fields
collect($rows)->toSelectable()->withValue('number')->withLabel('name')->toSelectOptions();

This is a good fit for status filters and for config()-driven dropdowns where spinning up a model is overkill.

Pattern 7: Test the output as a string

Because toSelectOptions() returns a string, assertions are straightforward and fast:

it('preselects the assignee', function () {
    $task = Task::factory()->for(User::factory()->state(['name' => 'Jane']))->create();

    $html = User::all()->toSelectOptions('name', 'id', $task->assignee_id);

    expect($html)->toContain('selected');
});

toSelectItems() is even better for logic tests — assert on selected/disabled booleans instead of grepping HTML, then keep one or two rendering smoke tests.

Pattern 8: Sort in SQL, decorate in the package

The package composes with collections rather than replacing them. Do ordering, filtering, and eager loading in the query; use the package only for presentation.

User::query()->orderBy('name')->get()   // sorting in SQL
    ->toSelectable()
    ->withLabel('name')                  // presentation only
    ->toSelectOptions();

Avoid ->sortBy(fn ($u) => ...) on the collection for large sets — it is PHP-side and will cost you more than an ORDER BY.


Gotchas and Tips

The output is unescaped HTML

toSelectOptions() returns a ready-made HTML fragment, so it must be printed with {!! !!}. If you print it with {{ }}, users see literal <option> tags on the page. Conversely, if any label is derived from user-controlled data (a name field, a title, a team name), it is your responsibility to escape it before it reaches the builder:

->withLabel(fn ($u) => e($u->display_name))

Audit this on any model whose display name can contain <, &, or quotes. A stray apostrophe in a label is harmless, but a stored <script> in a label field is not.

withSelected comparisons are effectively strict

The selected value must match the generated value. withValue('uuid') plus withSelected($user->id) silently selects nothing, and an integer 1 passed against a string '1' value may not match. Normalize to strings when in doubt:

->withSelected((string) $task->assignee_id)

For multi-selects, always pass an array (or a closure returning bool). Passing a single scalar against a multiple select is a common cause of "preselection doesn't work".

The README's disabled example uses = not ===

->withDisabled(fn($item) => $item->status = 'inactive')   // BUG in the README

That assigns 'inactive' to every model and returns a truthy string, so every option is disabled. Write the comparison:

->withDisabled(fn ($item) => $item->status === 'inactive')

Worth flagging in code review, because it is copied verbatim from the docs.

Missing label/value fields fail quietly

If your model has no name attribute (or you forgot withLabel), you get blank labels rather than an exception. Same for id on non-Eloquent collections — array collections need withValue() explicitly. If a dropdown renders as a column of empty options, check the label field before you check anything else.

Boolean data attributes serialize awkwardly

withDataAttribute('hidden', fn ($i) => $i->status !== 'active') renders data-hidden="1" or data-hidden="" depending on how PHP coerces the boolean. If your JS reads dataset.hidden === 'true', cast explicitly in the closure:

->withDataAttribute('hidden', fn ($i) => $i->status !== 'active' ? 'true' : 'false')

withId and toSelectItems() are not perfectly symmetric

withId was added in 1.0.4, later than the documented toSelectItems() structure. Do not assume the item array carries an id key. If your SPA needs per-option IDs, verify against the installed version's source or keep the ID in a dataAttributes entry instead.

Always ->get() first

toSelectable() is a collection macro. On a query builder you must materialize:

User::where('active', true)->toSelectOptions();        // ✗ Builder, not Collection
User::where('active', true)->get()->toSelectOptions(); // ✓

The error message on the failing case is unhelpful — "method does not exist" on an Eloquent Builder — so remember it is a Collection concern.

Large collections are HTML-string work

There is no chunking or lazy rendering: you get one string per call. For a few hundred options this is fine and demonstrably faster than a Blade loop (option rendering was optimized in 1.0.4). For thousands, use a searchable AJAX select instead, and use toSelectItems() or a paginated endpoint rather than shipping a 5,000-option string on every page load.

Cache the string for reference data

Country lists, categories, and role lists do not change per request:

$options = Cache::rememberForever('options.roles', fn () => Role::all()->toSelectOptions());

The string is deterministic given the same data, so it caches cleanly. Just remember to bust the key on save — a model observer is usually the least painful hook.

No config means no global defaults

Every call site repeats 'name', 'id', and class names. If that bothers you, define your own macro once in a service provider and standardize:

Collection::macro('toAppSelect', function (mixed $selected = null) {
    return $this->toSelectable()
        ->withLabel('name')
        ->withSelected($selected)
        ->withClass('form-option')
        ->toSelectOptions();
});

Macros are the intended extension point here — the package itself is built the same way.

Treat it as a small dependency

Five stars and zero dependents means the bus factor is real. The API is narrow enough that the honest risk assessment is: keep usage behind a Blade component or a macro (Patterns 2 and 1), and if you ever need to remove the package you can reimplement toSelectOptions in about thirty lines. Avoid scattering ->toSelectable()->withX() chains across fifty templates; that is what makes a small dependency expensive to leave.

Debugging checklist

When an option list looks wrong, check in this order:

  1. Is the collection actually a Collection (did you ->get())?
  2. Are the label and value fields present on every item, including array items?
  3. Is selected the same type as the generated value?
  4. Are closures returning booleans, not assignments?
  5. Is the Blade output using {!! !!}?

Four out of five of these are answered by dumping ->toSelectable()->toSelectItems() first — it shows you exactly what the package thinks each option is before HTML is involved.

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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style