Comboboxes
Combobox supports finite options, an Eloquent query serialized through the owning Inertia prop, or an application JSON endpoint. Finite options are available on the serialized field; useFormCombobox() manages asynchronous query and endpoint state.
Inline options
Finite options are serialized with the form resource. Your application renderer reads field.options and owns any local search, grouping, and presentation.
use Inertify\Form\Fields\Combobox;
Combobox::make('status', 'Status')
->options([
['value' => 'draft', 'label' => 'Draft'],
['value' => 'review', 'label' => 'In review'],
['value' => 'published', 'label' => 'Published'],
])
->searchable()
->clearable();
Options normalize to { value, label } and may also contain description, group, disabled, disabledReason, image or avatar data, badges, URLs, and metadata. Use mapping methods such as optionLabel(), optionValue(), optionDescription(), optionDisabled(), and groupBy() when source records use different keys.
By default, finite values receive membership validation. Use multiple(), minSelected(), maxSelected(), distinct(), or exists() to describe and validate multi-value behavior. allowCustomValues() and tokens() intentionally permit values outside a finite list.
Query-backed options
Pass an Eloquent model or query for request-aware search, paging, selected-option hydration, and membership validation.
use App\Models\Person;
use Inertify\Form\Fields\Combobox;
Combobox::make('assignee_id', 'Assignee')
->options(
Person::query()->where('active', true),
label: 'name',
value: 'id',
)
->perPage(25)
->searchable()
->preload();
The Vue controller reloads only the Inertia prop that owns the form and merges later pages. It assumes that prop is named form; identify a different key when creating the engine:
<Form :form="profileForm" :options="{ propKey: 'profileForm' }">
<!-- application-owned markup -->
</Form>
Each query-backed field gets a collision-safe options key, including qualified collection paths. Selected records are hydrated separately so an edit form can display values that are not in its current page.
Remote endpoints
Use source() when searching, grouping, hydration, or record creation belongs in an application endpoint.
Combobox::make('assignee_id', 'Assignee')
->source(route('people.index', absolute: false))
->selectedSource(route('people.index', absolute: false))
->searchParam('search')
->valuesParam('values')
->pageParam('page')
->perPage(25)
->minSearchLength(2)
->debounce(250)
->preload()
->searchable()
->createRecordUsing(
route('people.store', absolute: false),
'post',
'name',
);
The endpoint may return an option array or a paginator-shaped object whose data contains options. Search requests send the configured search and page parameters; selected hydration sends the configured values parameter. Creation may return one option directly or as { "item": option }.
{
"data": [
{ "value": 42, "label": "Ada Lovelace", "description": "Engineering" }
],
"current_page": 1,
"last_page": 3,
"next_page_url": "/people?page=2"
}
Authorize and rate-limit these routes like any other application endpoint. The package supplies the request transport, not the endpoint or its access policy.
Build the renderer
Create the controller with the field slot's qualified name and form. It manages search cancellation, loading, pagination, selected hydration, and record creation; your component decides how those states look and behave.
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import {
useChoices,
useFormCombobox,
type FormComboboxOption,
type FormFieldSlotProps,
} from '@inertify/form-vue'
const props = defineProps<FormFieldSlotProps>()
const choices = useChoices(props.name, props.form)
const combobox = useFormCombobox(props.name, props.form)
const availableOptions = computed<FormComboboxOption[]>(() => {
if (props.field.optionsMode === 'inertia' || props.field.source) {
return combobox.options.value
}
return Array.isArray(props.field.options)
? props.field.options as FormComboboxOption[]
: []
})
onMounted(() => void combobox.hydrateSelected())
</script>
<template>
<div v-if="visible">
<label :for="`combobox-${name}`">{{ field.label }}</label>
<input
:id="`combobox-${name}`"
v-model="combobox.search.value"
type="search"
:disabled="disabled"
>
<button
v-for="option in availableOptions"
:key="String(option.value)"
type="button"
:disabled="option.disabled === true"
@click="choices.select(option.value)"
>
{{ option.label }}
</button>
<button
v-if="combobox.hasMore.value"
type="button"
:disabled="combobox.loading.value"
@click="combobox.loadMore()"
>
Load more
</button>
</div>
</template>
The example uses .value because the refs are nested on the controller object. Destructure them into top-level script bindings if you prefer Vue's template ref unwrapping. Call combobox.create(label) for configured record creation and combobox.cancel() when your UI needs to stop outstanding work explicitly.