Forms
BelongsToSelect
BelongsToSelect is a relationship-aware select field for belongsTo associations.
On this page
- Basic Usage
- Common Options
- Scoped Options
- Inline Create
- Relationship write-back
- Auto-fill
- The matrix
- Owned 1:1 — hasOne / morphOne
- Owned 1:N — hasMany / morphMany
- Many-to-many with pivot — belongsToMany / morphToMany / morphedByMany
- Shared parent — belongsTo / morphTo
- Read-through — hasManyThrough / hasOneThrough
- Inside a Repeater
- Related Docs
BelongsToSelect is a relationship-aware select field for belongsTo associations.
Basic Usage
use NyonCode\WireForms\Components\BelongsToSelect; BelongsToSelect::make('company_id') ->relationship('company', 'name') ->label('Company') ->searchable()
This resolves options from the related model instead of requiring a manual options() array.
A searchable() select without preload() searches the related table on the server as the
user types (matching the title attribute, limited to 50 results) and resolves the selected
value's label with a single keyed lookup — the full option list never ships to the client.
Add preload() to load the whole list up front and filter client-side instead. An explicit
getSearchResultsUsing() callback (inherited from Select) overrides the built-in search.
Common Options
BelongsToSelect::make('company_id') ->relationship('company', 'name') ->searchable() ->preload() ->required()
| Method | Purpose |
|---|---|
relationship('company', 'name') |
Resolve options from the relation and title column |
searchable() |
Search the related table on the server as the user types |
preload() |
Load the full option list immediately and filter client-side |
modifyOptionsQueryUsing() |
Scope or sort the related model query |
createOptionForm() |
Show an inline create form for a new related record (auto-creates via the relation) |
createOptionUsing() |
Customize how a new option is persisted |
editOptionForm() |
Edit the selected related record — auto-fills and writes back through its relationships |
fillEditOptionUsing() / updateOptionUsing() |
Take full control of the edit fill / save (each receives the resolved $record) |
Scoped Options
BelongsToSelect::make('company_id') ->relationship('company', 'name') ->modifyOptionsQueryUsing(fn ($query) => $query->where('active', true))
Inline Create
use NyonCode\WireForms\Components\TextInput; BelongsToSelect::make('company_id') ->relationship('company', 'name') ->createOptionForm([ TextInput::make('name')->required(), ])
Inline create uses the same modal flow as Select: a "+ Create" button in the combobox panel opens a modal form, and on save the new record is created on the relationship (or via createOptionUsing()), selected, and merged into the dropdown without a page refresh. editOptionForm() from Select works here too.
With a relationship set, createOptionForm() auto-creates the related model even without createOptionUsing() — it introspects the belongsTo relation to find the related class and creates a record from the modal data. The field is handed the form's parent record by the runtime, which is what makes this (and every write-back below) work with no wiring.
Relationship write-back
editOptionForm() doesn't just show a modal — it reads from and writes to the related record through its own relationships. What the related model owns is written automatically; what it shares is left to an explicit callback. Every callback (fillEditOptionUsing, updateOptionUsing) receives the resolved related $record, so you can read or write through any relation without re-loading it.
Auto-fill
The edit modal is filled from the selected related record, pulling only the columns the edit form declares — never the id, timestamps, or unrelated columns:
BelongsToSelect::make('author_id') ->relationship('author', 'name') ->editOptionForm([ TextInput::make('name'), TextInput::make('email'), ])
A dotted field name walks a nested relation on fill (an empty relation yields null):
TextInput::make('company.name') // fills from $author->company->name
The matrix
On save, the kind of relation a dotted or repeater field targets decides how it is written:
| Relation | Field shape | Automatic write-back | Explicit alternative via updateOptionUsing($record) |
|---|---|---|---|
belongsTo, morphTo |
dotted origin.name |
fill only (shared parent) | $record->origin?->update(...), associate(), dissociate() |
hasOne, morphOne |
dotted profile.bio |
updateOrCreate (creates if missing) |
$record->profile()->save($model) |
hasMany, morphMany |
Repeater->relationship() |
sync: create / update / delete | $record->books()->saveMany([...]) |
belongsToMany, morphToMany, morphedByMany |
Repeater->relationship() + pivot columns |
pivot sync: attach / detach / update | syncWithoutDetaching(), toggle(), updateExistingPivot() |
hasManyThrough, hasOneThrough |
dotted / repeater | fill only (read-through) | write the intermediate yourself |
Polymorphic variants (morphOne, morphMany, morphTo, morphToMany, morphedByMany) behave exactly like their non-polymorphic base and set the polymorphic *_type/*_id on their own.
Owned 1:1 — hasOne / morphOne
A dotted field is written back through the relation, updated in place or created if missing:
BelongsToSelect::make('author_id') ->relationship('author', 'name') ->editOptionForm([ TextInput::make('name'), TextInput::make('profile.bio'), // hasOne TextInput::make('avatar.url'), // morphOne ])
To keep control, persist it yourself with the relation's save():
->updateOptionUsing(function ($record, array $data) { $avatar = $record->avatar ?? new Avatar; $avatar->url = data_get($data, 'avatar.url'); $record->avatar()->save($avatar); // stamps the morph type on a new record $record->update(['name' => $data['name']]);})
Owned 1:N — hasMany / morphMany
Edit the collection through a relationship-backed Repeater; the write-back syncs it (create new, update matched, delete removed):
use NyonCode\WireForms\Components\Repeater; BelongsToSelect::make('author_id') ->relationship('author', 'name') ->editOptionForm([ TextInput::make('name'), Repeater::make('books') ->relationship('books') ->schema([TextInput::make('title')]), ])
For a non-destructive write (create/update, never delete), use saveMany() in the callback:
->updateOptionUsing(function ($record, array $data) { $books = collect($data['books'])->map(function (array $row) { $book = ! empty($row['id']) ? Book::find($row['id']) : new Book; $book->title = $row['title']; return $book; })->all(); $record->books()->saveMany($books);})
Many-to-many with pivot — belongsToMany / morphToMany / morphedByMany
Each repeater item carries the related model's key plus pivot columns. The fill reads the pivot off each loaded row; the write-back sync()s it (attach / detach / update pivot columns):
BelongsToSelect::make('author_id') ->relationship('author', 'name') ->editOptionForm([ TextInput::make('name'), Repeater::make('tags') ->relationship('tags') ->schema([ TextInput::make('id'), // the related tag key TextInput::make('role'), // a pivot column (declared with ->withPivot('role')) ]), ])
Both a custom pivot model (->using(...), so its casts apply) and an ad-hoc pivot (just ->withPivot(...), the framework's plain Pivot/MorphPivot) work unchanged, because the flow runs at the relation level. For a different pivot strategy, drop into the callback:
->updateOptionUsing(function ($record, array $data) { $sync = collect($data['tags']) ->mapWithKeys(fn ($row) => [$row['id'] => ['role' => $row['role']]]) ->all(); $record->tags()->syncWithoutDetaching($sync); // attach/update, never detach // $record->tags()->toggle([$id1, $id2]); // flip membership of the listed ids // $record->tags()->updateExistingPivot($id, [...]); // pivot columns of an attached row only})
Shared parent — belongsTo / morphTo
A belongsTo/morphTo points at a record the related model doesn't own (many authors can share one company), so it is never mutated automatically — the fill loads it for display and you write it explicitly:
BelongsToSelect::make('author_id') ->relationship('author', 'name') ->editOptionForm([ TextInput::make('name'), TextInput::make('company.name'), ]) ->updateOptionUsing(function ($record, array $data) { $record->update(['name' => $data['name']]); $record->company?->update(['name' => data_get($data, 'company.name')]); })
Re-pointing the association is either an own-column edit (the foreign key is a real column, so the auto write-back handles it) or an idiomatic associate() / dissociate():
->updateOptionUsing(function ($record, array $data) { $record->company()->associate(Company::find($data['company_id'])); // re-point // $record->company()->dissociate(); // detach $record->save();})
For a morphTo, associate() re-points a different type (it sets both *_type and *_id); dissociate() nulls both.
Read-through — hasManyThrough / hasOneThrough
A read-through relation has no unambiguous write-back (which intermediate parent would own a new distant row is unknowable), so the fill still loads it for display but it is never auto-written — no orphaned or deleted rows. Write it explicitly through the intermediate in updateOptionUsing($record) when you need to.
Inside a Repeater
Because the runtime propagates the parent record to relationship fields everywhere, all of the above works the same for a BelongsToSelect nested inside a Repeater item, per item.