ajjitech/searchable-select
Composer 安装命令:
composer require ajjitech/searchable-select
包简介
A powerful, Bootstrap 5 searchable dropdown component for Laravel Livewire 3 & 4 applications. Built with Alpine.js — no jQuery, no extra JS dependencies.
关键字:
README 文档
README
A powerful, feature-rich searchable dropdown component for Laravel Livewire 3 & 4 applications. Styled with Bootstrap 5 and powered by Alpine.js (bundled with Livewire) — no jQuery, no extra JavaScript dependencies.
Table of Contents
- Features
- Requirements
- Installation
- Bootstrap Setup
- Quick Start
- Component Props Reference
- Usage Examples
- Advanced Features
- Customization Guide
- Dark Mode
- Troubleshooting
- Performance Optimization
- Testing
- Demo Application
- Contributing
- Credits
- License
Features
- Real-time search — client-side filtering as you type
- Multi-select support — select multiple options with visual badge tags
- Grouped options — organize options into labeled categories
- Clear button — one-click to clear all selections
- Dark mode support — automatic via Bootstrap 5.3 CSS variables (
data-bs-theme="dark") - Accessible — full keyboard navigation and ARIA attributes
- Livewire 3 & 4 compatible — works seamlessly with both versions
- Responsive — mobile-friendly and touch-optimized
- Disabled state — conditional disabling support
- Flexible data — works with Eloquent models, arrays, collections
- Dependent dropdowns — perfect for cascading country → region → city selects
- Bootstrap-native — uses Bootstrap CSS variables, no extra CSS framework required
- Zero extra JS — Alpine.js is bundled with Livewire; no jQuery, no CDN scripts
Requirements
- PHP: 8.2 or higher
- Laravel: 11.x, 12.x, or 13.x
- Livewire: 3.x or 4.x
- Bootstrap: 5.3+
- Alpine.js: bundled with Livewire (no separate install needed)
Installation
Install the package via Composer:
composer require ajjitech/searchable-select
The package automatically registers its service provider. You can start using it immediately.
To customize the component HTML, publish the view:
php artisan vendor:publish --tag=searchable-select-views
This copies the view to resources/views/vendor/searchable-select/searchable-select.blade.php.
Bootstrap Setup
The component is styled entirely with Bootstrap 5.3 CSS variables and scoped .ss-* custom classes embedded in the view. You do not need to configure any content scanning or build step for the component styles — they are injected inline via @once automatically.
You simply need Bootstrap 5.3+ loaded in your application.
Option A — npm (recommended)
npm install bootstrap
Import in your resources/js/app.js:
import 'bootstrap/dist/css/bootstrap.min.css'; // Bootstrap JS is optional — the component uses Alpine.js for all interactivity
Or in your resources/css/app.css:
@import "bootstrap/dist/css/bootstrap.min.css";
Option B — CDN
Add Bootstrap's CSS to your layout <head>:
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" >
Note: Bootstrap JS (
bootstrap.bundle.min.js) is not required. The component manages all its own interactivity through Alpine.js.
Quick Start
Basic Usage
Step 1: Create a Livewire component
php artisan make:livewire ContactForm
Step 2: Set up the component class
<?php namespace App\Livewire; use App\Models\Country; use Livewire\Component; class ContactForm extends Component { public $countries; public $country_id; public function mount() { $this->countries = Country::orderBy('name')->get(); } public function save() { $this->validate([ 'country_id' => 'required|exists:countries,id', ]); // Save your data... } public function render() { return view('livewire.contact-form'); } }
Step 3: Use the component in your Blade view
<div> <label class="form-label">Country</label> <x-searchable-select wire:model="country_id" :options="$countries" placeholder="Select a country" search-placeholder="Type to search countries..." /> @error('country_id') <div class="invalid-feedback d-block">{{ $message }}</div> @enderror <button wire:click="save" class="btn btn-primary mt-3">Save</button> </div>
That's it. The component syncs with your Livewire property via wire:model automatically.
Component Props Reference
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
options |
Array/Collection | [] |
Yes | Options data source |
placeholder |
String | 'Select option' |
No | Text shown when nothing is selected |
searchPlaceholder |
String | 'Search...' |
No | Placeholder for the search input |
emptyMessage |
String | 'No options available' |
No | Message shown when the list is empty |
optionValue |
String | 'id' |
No | Key to use as the option value |
optionLabel |
String | 'name' |
No | Key to use as the display label |
multiple |
Boolean | false |
No | Enable multi-select mode |
clearable |
Boolean | true |
No | Show/hide the × clear button |
disabled |
Boolean | false |
No | Disable the dropdown |
grouped |
Boolean | false |
No | Enable grouped options mode |
groupLabel |
String | 'label' |
No | Key for group label (when grouped is true) |
groupOptions |
String | 'options' |
No | Key for group items array (when grouped is true) |
teleport |
Boolean | true |
No | Teleport the dropdown panel to <body> to avoid clipping by parent containers. Set to false inside Bootstrap modals. |
wire:modelis a standard Livewire directive. Pass it aswire:model="propertyName"and the component handles two-way binding automatically.
Usage Examples
Basic Single Select
class UserProfile extends Component { public $countries; public $country_id; public function mount() { $this->countries = Country::orderBy('name')->get(); } }
<x-searchable-select wire:model="country_id" :options="$countries" placeholder="Select your country" search-placeholder="Search countries..." />
Multi-Select
Select multiple options displayed as removable badge tags:
class UserSkills extends Component { public $skills; public array $selected_skills = []; public function mount() { $this->skills = Skill::orderBy('name')->get(); } }
<x-searchable-select wire:model="selected_skills" :options="$skills" :multiple="true" placeholder="Select your skills" /> @if (!empty($selected_skills)) <p class="mt-2 text-muted small">{{ count($selected_skills) }} skills selected</p> @endif
Selected items appear as Bootstrap-styled badge tags with × remove buttons.
Dependent/Cascading Dropdowns
class LocationSelector extends Component { public $countries; public $regions = []; public $cities = []; public $country_id; public $region_id; public $city_id; public function mount() { $this->countries = Country::orderBy('name')->get(); } public function updatedCountryId($value) { $this->regions = Region::where('country_id', $value)->orderBy('name')->get(); $this->region_id = null; $this->city_id = null; $this->cities = []; } public function updatedRegionId($value) { $this->cities = City::where('region_id', $value)->orderBy('name')->get(); $this->city_id = null; } }
<div class="row g-3"> <div class="col-md-4"> <label class="form-label">Country</label> <x-searchable-select wire:model="country_id" :options="$countries" placeholder="Select Country" /> </div> <div class="col-md-4"> <label class="form-label">Region</label> <x-searchable-select wire:model="region_id" :options="$regions" :placeholder="empty($regions) ? 'First select a country' : 'Select Region'" :disabled="!$country_id" /> </div> <div class="col-md-4"> <label class="form-label">City</label> <x-searchable-select wire:model="city_id" :options="$cities" :placeholder="empty($cities) ? 'First select a region' : 'Select City'" :disabled="!$region_id" /> </div> </div>
Grouped Options
public $locations = [ [ 'label' => 'North America', 'options' => [ ['id' => 1, 'name' => 'United States'], ['id' => 2, 'name' => 'Canada'], ['id' => 3, 'name' => 'Mexico'], ], ], [ 'label' => 'Europe', 'options' => [ ['id' => 4, 'name' => 'United Kingdom'], ['id' => 5, 'name' => 'France'], ['id' => 6, 'name' => 'Germany'], ], ], ];
<x-searchable-select wire:model="country_id" :options="$locations" :grouped="true" placeholder="Select a country" />
Custom group keys:
public $categories = [ [ 'category_name' => 'Fruits', 'items' => [ ['code' => 'APL', 'title' => 'Apple'], ['code' => 'BAN', 'title' => 'Banana'], ], ], ];
<x-searchable-select wire:model="selected_item" :options="$categories" :grouped="true" group-label="category_name" group-options="items" option-value="code" option-label="title" />
Custom Keys
public $products = [ ['sku' => 'PROD-001', 'product_name' => 'Laptop'], ['sku' => 'PROD-002', 'product_name' => 'Mouse'], ]; public $selected_sku;
<x-searchable-select wire:model="selected_sku" :options="$products" option-value="sku" option-label="product_name" placeholder="Select a product" />
Bootstrap Modal
When using the component inside a Bootstrap modal, disable teleporting so the dropdown stays inside the modal's DOM and follows Bootstrap's focus and stacking context correctly.
<div class="modal fade" id="demoModal" tabindex="-1" aria-hidden="true" wire:ignore.self> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-body"> <label class="form-label">Country Inside Modal</label> <x-searchable-select wire:model.live="selectedCountry" :options="$countries" option-label="name" option-value="id" placeholder="Search country..." :teleport="false" /> </div> </div> </div> </div>
With Validation
public function save() { $this->validate([ 'country_id' => 'required|exists:countries,id', ]); }
<div class="mb-3"> <label class="form-label">Country <span class="text-danger">*</span></label> <x-searchable-select wire:model="country_id" :options="$countries" /> @error('country_id') <div class="invalid-feedback d-block">{{ $message }}</div> @enderror </div> <button wire:click="save" class="btn btn-primary">Save</button>
Real-time validation:
public function updated($propertyName) { $this->validateOnly($propertyName); }
Disabled State
<x-searchable-select wire:model="region_id" :options="$regions" :disabled="!$country_id" placeholder="First select a country" />
Without Clear Button
<x-searchable-select wire:model="country_id" :options="$countries" :clearable="false" />
Using Arrays Instead of Models
public $statuses = [ ['id' => 'draft', 'name' => 'Draft'], ['id' => 'published', 'name' => 'Published'], ['id' => 'archived', 'name' => 'Archived'], ];
<x-searchable-select wire:model="status" :options="$statuses" />
Advanced Features
Adding Extra CSS Classes
Extra attributes (including class) are forwarded to the trigger element:
<x-searchable-select wire:model="country_id" :options="$countries" class="border-primary" />
Creating Specialized Components
resources/views/components/country-select.blade.php:
@props(['wireModel']) <x-searchable-select wire:model="{{ $wireModel }}" :options="\App\Models\Country::orderBy('name')->get()" placeholder="Select a country" search-placeholder="Search countries..." {{ $attributes }} />
Usage:
<x-country-select wire-model="country_id" />
Server-Side Search (Large Datasets)
For thousands of records, implement server-side search in Livewire:
public $searchTerm = ''; public $countries = []; public function updatedSearchTerm($value) { $this->countries = Country::where('name', 'like', "%{$value}%") ->limit(50) ->get(); }
Customization Guide
Publishing Views
php artisan vendor:publish --tag=searchable-select-views
Your copy lands in resources/views/vendor/searchable-select/searchable-select.blade.php. Laravel uses your copy instead of the package default.
All component styles are scoped under .ss-* CSS classes defined in the @once style block at the top of the view. You can override any of them after publishing.
Overriding Individual Styles
After publishing the view, you can add custom CSS in your app.css:
/* Make the trigger taller */ .ss-trigger { min-height: 48px; } /* Change tag badge color */ .ss-tag { background-color: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis); } /* Change selected option highlight */ .ss-option.ss-selected { background-color: var(--bs-success-bg-subtle); color: var(--bs-success-text-emphasis); }
Dark Mode
Dark mode works automatically via Bootstrap 5.3's CSS variable system. Add data-bs-theme="dark" to your <html> element:
<html data-bs-theme="dark">
Bootstrap's CSS variables (--bs-body-bg, --bs-body-color, --bs-border-color, --bs-primary-bg-subtle, etc.) adjust automatically, and the component adapts with them. No additional configuration needed.
Troubleshooting
Dropdown doesn't open / Click doesn't work
-
Verify Alpine.js is loaded (it comes bundled with Livewire 3+):
@livewireScripts {{-- This includes Alpine.js --}}
-
Ensure you are not loading Alpine.js separately when using Livewire 3+:
<!-- Remove this if you have Livewire 3+ --> <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
-
Check the browser console for JavaScript errors (F12 → Console).
Component appears unstyled
- Ensure Bootstrap 5.3+ CSS is loaded before your page content renders.
- Verify Bootstrap's
dist/css/bootstrap.min.cssis included, not just Bootstrap JS. - Clear the Laravel view cache:
php artisan view:clear
Options not updating / Stale data
Use wire:key when rendering multiple components in loops:
@foreach($forms as $form) <x-searchable-select wire:key="country-{{ $form->id }}" wire:model="forms.{{ $loop->index }}.country_id" :options="$countries" /> @endforeach
Multi-select not working
Initialize the Livewire property as an array:
// Correct public array $selected_items = []; // Wrong — null, not an array public $selected_items;
Enable multiple mode in the blade:
<x-searchable-select :multiple="true" wire:model="selected_items" :options="$items" />
Selected value not displaying
Verify the selected value exists in your options and the optionValue key matches:
// If your data uses 'code' instead of 'id' $countries = [['code' => 'US', 'name' => 'USA']];
<x-searchable-select wire:model="country_code" :options="$countries" option-value="code" />
Performance Optimization
| Options Count | Recommended Approach |
|---|---|
| < 100 | Client-side filtering (default) — works perfectly |
| 100 – 1,000 | Client-side filtering with wire:key — still performant |
| 1,000+ | Server-side search with Livewire updated* hooks |
Server-side search:
public $searchTerm = ''; public $products = []; public function updatedSearchTerm($value) { $this->products = Product::where('name', 'like', "%{$value}%") ->limit(50) ->get(); }
Select only needed columns:
// Good — only id and name $this->users = User::select('id', 'name')->get();
Cache static options:
public function mount() { $this->countries = Cache::remember('countries', 3600, fn () => Country::orderBy('name')->get() ); }
Testing
The package includes a comprehensive test suite.
# Run all tests composer test # Run with coverage composer test -- --coverage # Run specific test file ./vendor/bin/pest tests/Feature/ComponentTest.php
Demo Application
The package includes a full-featured demo application.
cd demo
composer install
cp .env.example .env
php artisan key:generate
php artisan migrate
php artisan serve
Visit http://localhost:8000
Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Make your changes and add tests
- Run tests and code style checks:
composer test composer format - Commit and open a Pull Request
Code Style
- Laravel Pint for PHP formatting
- PSR-12 coding standard
- Pest PHP for testing
Credits
Maintainer
- Ajjitech — GitHub
Original Author
This package is a Bootstrap 5 port of williamug/searchable-select, originally created by William Asaba — GitHub.
Built With
- Laravel — The PHP Framework
- Livewire — Full-stack framework for Laravel
- Alpine.js — Lightweight JavaScript framework
- Bootstrap — CSS framework
License
The MIT License (MIT). Please see License File for more information.
Links
- Packagist — Composer package
- GitHub Repository — Source code
- Issue Tracker — Report bugs or request features
- Changelog — Version history
Made with care for the Laravel community
ajjitech/searchable-select 适用场景与选型建议
ajjitech/searchable-select 是一款 基于 Blade 开发的 Composer 扩展包,目前已累计 3 次下载、GitHub Stars 达 0, 最近一次更新时间为 2026 年 05 月 25 日, 在 PHP 生态内属于活跃度较高的组件。
它主要适用于以下技术方向: 「bootstrap」 「laravel」 「select」 「select2」 「dropdown」 「livewire」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。
我们在过去多个企业项目中使用过 ajjitech/searchable-select 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。
基于 ajjitech/searchable-select 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。
线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。
承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。
与 ajjitech/searchable-select 相关的其它包
同方向 / 同关键字的高下载量 PHP Composer 包推荐,方便对比选型:
CakePHP behavior plugin for easily generating some complicated queries like (bulk) insert/upsert etc.
PHP PDO Mysql select statement iterator implemented as multiple queries using LIMIT clauses
PHP library that allows linking queries from diferent physical databases using mysql pdo database connections
Country select field type.
Views for the package MedicOneSystems Livewire Datatables with Bootstrap 4
bootstrap select field module implementing http://silviomoreto.github.io/bootstrap-select/
统计信息
- 总下载量: 3
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 25
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: MIT
- 更新时间: 2026-05-25