Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,67 @@ return [

## Usage

### Basic Usage

```php
use Prvious\Filament\Combobox\Components\Combobox;

Combobox::make('status')
->options([
'draft' => 'Draft',
'published' => 'Published',
'archived' => 'Archived',
])
```

### Setting a Search Query

You can set a search query that will be displayed in the search input when the component loads:

```php
Combobox::make('product')
->searchable()
->options([
// your options
])
->searchQuery('electronics')
```

This will populate the search input with "electronics" but will not trigger a search automatically.

### Auto-Triggering a Search on Load

If you want to perform a search automatically when the component boots, use the `autoSearch()` method along with `searchQuery()`:

```php
Combobox::make('product')
->searchable()
->getSearchResultsUsing(function (string $search) {
return Product::where('name', 'like', "%{$search}%")
->limit(50)
->pluck('name', 'id')
->toArray();
})
->searchQuery('electronics')
->autoSearch()
```

This is useful when you want to pre-filter options based on context, such as:
- Showing products from a specific category
- Filtering items based on user permissions
- Displaying search results based on a related field value

You can also conditionally enable auto-search:

```php
Combobox::make('product')
->searchable()
->searchQuery($categoryName)
->autoSearch(filled($categoryName))
```

**Note:** The `autoSearch()` method requires that the component is searchable and has a search handler configured.

## Testing

```bash
Expand Down
2 changes: 1 addition & 1 deletion resources/dist/components/combobox.js

Large diffs are not rendered by default.

75 changes: 74 additions & 1 deletion resources/js/components/combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ class Combobox {
maxItemsMessage = 'Maximum number of items selected',
optionsLimit = null,
searchableOptionFields = ['label'],
searchQuery = null,
autoSearch = false,
livewireId = null,
statePath = null,
onStateChange = () => {},
Expand Down Expand Up @@ -78,13 +80,15 @@ class Combobox {
this.searchableOptionFields = Array.isArray(searchableOptionFields)
? searchableOptionFields
: ['label']
this.initialSearchQuery = searchQuery
this.autoSearch = autoSearch
this.livewireId = livewireId
this.statePath = statePath
this.onStateChange = onStateChange

this.labelRepository = {}
this.selectedIndex = -1
this.searchQuery = ''
this.searchQuery = searchQuery || ''
this.searchTimeout = null
this.isSearching = false
this.selectedDisplayVersion = 0
Expand All @@ -95,6 +99,10 @@ class Combobox {
if (this.isAutofocused && this.searchInput) {
this.searchInput.focus()
}

if (this.autoSearch && filled(this.initialSearchQuery)) {
this.performInitialSearch()
}
}

populateLabelRepositoryFromOptions(options) {
Expand Down Expand Up @@ -139,6 +147,10 @@ class Combobox {
this.searchInput.type = 'text'
this.searchInput.placeholder = this.searchPrompt
this.searchInput.setAttribute('aria-label', 'Search')

if (this.searchQuery) {
this.searchInput.value = this.searchQuery
}

this.searchContainer.appendChild(this.searchInput)
}
Expand Down Expand Up @@ -197,6 +209,63 @@ class Combobox {
}
}

async performInitialSearch() {
if (!this.isSearchable || !this.searchInput) {
return
}

const query = this.initialSearchQuery

if (!filled(query)) {
return
}

this.searchQuery = query
this.searchInput.value = query

if (
!this.getSearchResultsUsing ||
typeof this.getSearchResultsUsing !== 'function' ||
!this.hasDynamicSearchResults
) {
this.filterOptions(query.trim().toLowerCase())
return
}

this.isSearching = true

try {
this.showLoadingState(true)

const results = await this.getSearchResultsUsing(query)

const normalizedResults = Array.isArray(results)
? results
: results && Array.isArray(results.options)
? results.options
: []

this.options = normalizedResults

this.populateLabelRepositoryFromOptions(normalizedResults)

this.hideLoadingState()
this.renderOptions()

if (this.options.length === 0) {
this.showNoResultsMessage()
}
} catch (error) {
console.error('Error fetching initial search results:', error)

this.hideLoadingState()
this.options = JSON.parse(JSON.stringify(this.originalOptions))
this.renderOptions()
} finally {
this.isSearching = false
}
}

renderOptions() {
this.optionsList.innerHTML = ''

Expand Down Expand Up @@ -1538,6 +1607,8 @@ export default function comboboxFormComponent({
searchingMessage,
searchPrompt,
searchableOptionFields,
searchQuery,
autoSearch,
state,
statePath,
}) {
Expand Down Expand Up @@ -1577,6 +1648,8 @@ export default function comboboxFormComponent({
maxItemsMessage,
optionsLimit,
searchableOptionFields,
searchQuery,
autoSearch,
livewireId,
statePath,
onStateChange: (newState) => {
Expand Down
2 changes: 2 additions & 0 deletions resources/views/components/combobox.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ class="fi-hidden"
searchingMessage: @js($getSearchingMessage()),
searchPrompt: @js($getSearchPrompt()),
searchableOptionFields: @js($getSearchableOptionFields()),
searchQuery: @js($getSearchQuery()),
autoSearch: @js($shouldAutoSearch()),
state: $wire.{{ $applyStateBindingModifiers("\$entangle('{$statePath}')") }},
statePath: @js($statePath),
})"
Expand Down
29 changes: 29 additions & 0 deletions src/Components/Combobox.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Prvious\Filament\Combobox\Components;

use Closure;
use Filament\Forms\Components\Select;

class Combobox extends Select
Expand All @@ -10,4 +11,32 @@ class Combobox extends Select
* @var view-string
*/
protected string $view = 'prvious-combobox::components.combobox';

protected string | Closure | null $searchQueryValue = null;

protected bool $shouldAutoSearch = false;

public function searchQuery(string | Closure | null $query): static
{
$this->searchQueryValue = $query;

return $this;
}

public function getSearchQuery(): ?string
{
return $this->evaluate($this->searchQueryValue);
}

public function autoSearch(bool | Closure $condition = true): static
{
$this->shouldAutoSearch = $condition;

return $this;
}

public function shouldAutoSearch(): bool
{
return $this->evaluate($this->shouldAutoSearch);
}
}