Skip to main content

Search Combobox

A searchable dropdown component with infinite scroll, supporting both domain objects and custom search implementations. The Search Combobox provides a consistent interface for entity selection with backend-powered search.

Component Variants

SearchCombobox

Standalone component for use outside of forms.
<SearchCombobox
  value={selectedId}
  onChange={setSelectedId}
  domainObject="App\\Models\\User"
  placeholder={__('Select user...')}
/>

CoreFormSearchCombobox

Integrated with Inertia forms for automatic form state management.
<CoreFormSearchCombobox
  name="user_id"
  label={__('User')}
  searchComboboxProps={{
    domainObject: 'App\\Models\\User',
  }}
  placeholder={__('Search users...')}
/>

Multiple ID Support

The SearchQueryBuilder now supports multiple IDs for future-proofing multi-select functionality. The implementation always uses whereIn for consistency - single values are automatically converted to arrays with one element. Note: Missing values are silently filtered out rather than throwing exceptions, making the API more resilient to stale or invalid IDs.

Backend Implementation

// Always use whereIn - single values become arrays automatically
->whenValue(fn(array $value) => Model::query()->whereIn('id', $value))

// Optionally specify how to key the results (defaults to model's getKey())
->keyBy(fn($model) => $model->custom_key)

Default Keying Behavior

  • Regular Eloquent models: Uses $model->getKey() (typically the primary key)
  • Domain objects: Uses $model->getObjectIdKeyName() value
  • Custom: Override with ->keyBy(fn($model) => $model->your_key)

Return Types

Always returns a keyed Collection<ObjectData> (or keyed array in JSON):
  • Single value input: Returns keyed collection with one item (or empty if not found)
  • Multiple values input: Returns keyed collection with found items (missing values are silently filtered out)
Example responses:
// Single value found
{"123": {"id": "123", "title": "John Doe", ...}}

// Single value not found
{}

// Multiple values (some found)
{"123": {"id": "123", "title": "John Doe", ...}, "456": {"id": "456", "title": "Jane Smith", ...}}
The DomainObjectSearchQueryBuilder automatically handles this pattern in its default implementation.

Usage Patterns

Use the domain object’s built-in global search by passing the fully qualified class name. Model must implement DomainObjectContract (typically via HasDomainObject trait) and use Searchable for Scout integration.
<CoreFormSearchCombobox
  name="user_id"
  label={__('User')}
  searchComboboxProps={{
    domainObject: 'App\\Models\\User',
  }}
  placeholder={__('Search users...')}
/>
The component will automatically call the core SearchController which handles domain object search. When you need additional filtering beyond the model’s default search behavior, create a custom controller with DomainObjectSearchQueryBuilder.

Custom Controller

use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Inly\Core\Services\Search\DomainObjectSearchQueryBuilder;

class UserSearchController extends Controller
{
    public function __invoke(Request $request)
    {
        $departmentId = $request->input('department_id');

        return DomainObjectSearchQueryBuilder::make($request)
            ->forDomainObject(User::class)
            ->applyScope(function (Builder $query) use ($departmentId) {
                return $query
                    ->where('is_active', true)
                    ->when($departmentId, fn($q) => $q->where('department_id', $departmentId));
            })
            ->results();
    }
}

Route

Route::get('/api/users/search', UserSearchController::class)
    ->name('users.search');

Frontend Usage

<CoreFormSearchCombobox
  name="user_id"
  label={__('User')}
  searchComboboxProps={{
    searchUrl: route('users.search', {
      department_id: form.data.department_id,
    }),
  }}
  placeholder={__('Search users...')}
/>
For models that don’t implement domain objects (e.g., third-party data, external APIs), use SearchQueryBuilder with custom mapping.

Custom Controller

use App\Enums\Permission;
use App\Models\BusinessCentralCustomer;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Inly\Core\Data\ObjectData;
use Inly\Core\Services\Search\SearchQueryBuilder;

class BusinessCentralCustomerSearchController extends Controller
{
    public function __invoke(Request $request)
    {
        $this->authorize(Permission::MANAGE_ORDERS->value);

        $company = $request->input('company');

        return SearchQueryBuilder::make($request)
            ->whenEmpty(fn() => BusinessCentralCustomer::query()->orderBy('name'))
            ->whenSearch(fn(string $searchTerm) => BusinessCentralCustomer::search($searchTerm))
            ->whenValue(fn(array $value) => BusinessCentralCustomer::query()->whereIn('id', $value))
            ->keyBy(fn($model) => $model->id) // Optional: specify custom keying
            ->applyScope(function (Builder $query) use ($company) {
                return $query
                    ->when($company, fn($q) => $q->environment($company))
                    ->whereNotNull('name')
                    ->whereNot('name', '');
            })
            ->mapAs(fn(BusinessCentralCustomer $customer) => ObjectData::from([
                'id' => $customer->id,
                'title' => $customer->name,
                'subtitle' => $customer->customer_number,
                'icon' => 'Building',
            ]))
            ->results();
    }
}

Route

Route::get('/api/business-central-customers/search', BusinessCentralCustomerSearchController::class)
    ->name('business-central-customers.search');

Frontend Usage

<CoreFormSearchCombobox
  searchComboboxProps={{
    buttonProps: {
      className: 'w-full',
    },
    searchUrl: route('business-central-customers.search', {
      company: form.data.company,
    }),
  }}
  name="customer_id"
  label={__('Customer')}
  disabled={!form.data.company}
  placeholder={__('Search customers...')}
/>