Options Endpoints
Select yang menggunakan relation hanya merender satu halaman data yang dibatasi, padahal related table dapat berisi ribuan row. Options endpoint membuat row lainnya tetap dapat dicari: browser mengirim search term dan nama field, lalu server mengembalikan pasangan value/label. Gunakan dokumentasi ini ketika sebuah Select perlu searchable, atau ketika Anda perlu mengetahui secara tepat apa saja yang boleh dituju sebuah search request.
Contoh minimal
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
public static function form(FormSchema $schema): FormSchema
{
return $schema->schema([
Select::make('author')
->relationship('author', 'name')
->searchable()
->required(),
]);
}2
3
4
5
6
7
8
9
10
11
12
Itu saja yang dibutuhkan. Resource Page membangun endpoint URL, mengirimkannya bersama form, lalu field melakukan request ketika user mengetik. Informasi table, column, dan model tidak pernah dikirim ke browser.
Select, method per method
PandaPanel\Forms\Components\Select bersifat final dan extends Field, sehingga seluruh behavior pada FormSchema basics tetap berlaku.
| Method | Signature | Default |
|---|---|---|
options() | options(array<array-key, string> $options): self | [] |
relationship() | relationship(string $relation, string $titleAttribute): self | tidak ada |
existsIn() | existsIn(string $table, string $column): self | diturunkan dari relation |
searchable() | searchable(bool $searchable = true): self | false |
multiple() | multiple(bool $multiple = true): self | false, otomatis true untuk many-to-many |
optionLimit() | optionLimit(int $limit): self | 50, minimum 1 |
getRelation() | getRelation(): ?string | |
isMultiple() | isMultiple(): bool | |
writesToPivot() | writesToPivot(string $modelClass): bool | true untuk BelongsToMany dan MorphToMany |
foreignKeyFor() | foreignKeyFor(string $modelClass): ?string | non-null untuk BelongsTo |
relatedKeys() | relatedKeys(Model $record): list<string> | key yang sedang terpasang |
resolveOptions() | resolveOptions(?string $modelClass = null, ?string $search = null): list<array{value: string, label: string}> | |
hydrateRelationship() | hydrateRelationship(string $modelClass): void | dipanggil oleh schema |
Static options
use PandaPanel\Forms\Components\Select;
Select::make('status')->options([
'draft' => 'Draft',
'published' => 'Published',
]);2
3
4
5
6
Key merupakan value sekaligus whitelist. Submitted value wajib salah satu key tersebut melalui Rule::in. Key dibandingkan sebagai string, sehingga integer key dan string yang dikirim form dianggap value yang sama.
Relation options
use PandaPanel\Forms\Components\Select;
Select::make('author')->relationship('author', 'name'); // BelongsTo
Select::make('labels')->relationship('labels', 'name'); // BelongsToMany2
3
4
relationship(string $relation, string $titleAttribute) menunjuk method relation pada model milik schema dan attribute yang dipakai sebagai label row. FormSchema me-resolve relation tersebut karena hanya layer inilah yang mengetahui model class. Proses ini melakukan tiga hal:
BelongsToManyatauMorphToManymengubah field menjadi multiple select.existsIn()diisi dari related table dan key, kecuali Anda telah menetapkannya sendiri.- Halaman pertama options di-resolve lalu diserialisasi.
Field BelongsTo dinamai berdasarkan relation dan dipersist ke foreign key-nya. Karena itu form tidak perlu menambahkan ->dehydrateTo('author_id') di samping ->relationship('author'). Many-to-many tidak memiliki column pada owner; field dikeluarkan dari dehydrate() dan pivot disinkronkan oleh FormSchema::saveRelations() setelah record owner tersedia. Lihat Relationship forms.
Relation yang method-nya tidak ada pada model, atau method yang tidak mengembalikan Eloquent relation, menghasilkan InvalidArgumentException yang menyebutkan nama field.
resolveOptions()
use App\Models\Post;
use PandaPanel\Forms\Components\Select;
$field = Select::make('author')->relationship('author', 'name')->optionLimit(20);
$field->resolveOptions(Post::class); // 20 pertama, diurutkan berdasarkan name
$field->resolveOptions(Post::class, 'ada'); // hasil yang cocok, tetap dibatasi2
3
4
5
6
7
Query menggunakan where({title}, 'like', '%term%') dengan \, %, dan _ di-escape, diurutkan berdasarkan title attribute, dibatasi oleh optionLimit(), lalu diambil sebagai key => title. Key menjadi value, title menjadi label.
Memanggil method ini untuk relation tanpa model class menghasilkan InvalidArgumentException. Untuk static options, model class tidak dibutuhkan dan search term diabaikan.
existsIn()
use PandaPanel\Forms\Components\Select;
Select::make('country_code')
->options(['id' => 'Indonesia'])
->existsIn('countries', 'code');2
3
4
5
Dengan existsIn(), validitas bukan lagi sekadar apakah value ada pada options yang sedang ditampilkan, tetapi apakah row tersebut benar-benar ada pada table. Relation mengisi konfigurasi ini otomatis. Secara manual, ini berguna ketika UI hanya menampilkan shortlist dari table yang jauh lebih besar.
Endpoint
Route bernama panel.{panel_id}.options, satu per Panel, dan ditangani oleh PandaPanel\Http\Controllers\PanelFormOptionsController. Request berupa GET dan response berupa JSON:
{ "options": [ { "value": "1", "label": "Ada Lovelace" } ] }| Query parameter | Arti |
|---|---|
resource | Slug resource pada Panel. 422 jika tidak ada, 404 jika tidak dikenal |
field | Nama field. 422 jika tidak ada, 404 jika schema tidak mendeklarasikannya |
search | Di-trim dan dipotong maksimum 255 karakter. Tidak ada berarti tanpa filter |
page | create atau edit. Value lain menghasilkan 422 |
relation | Ada pada relation form: key milik relation manager |
operation | Relation operation. 404 jika tidak dikenal |
record | Record resource yang sedang diedit, atau owner key pada relation form |
related | Related record untuk operation yang membutuhkannya |
Context—resource, Page, relation, operation, owner—dibentuk server melalui PandaPanel\Support\FormEndpoints dan dikirim di URL. Browser hanya menambahkan field dan search. Pembagian ini memastikan keystroke tidak dapat mengubah form atau operation yang sedang diminta.
use PandaPanel\Support\FormEndpoints;
FormEndpoints::forResource(PostResource::class, 'edit', $post);
// /admin/options?resource=posts&page=edit&record=1
FormEndpoints::forRelation(PostResource::class, CommentsRelationManager::class, $post, 'attach');
// /admin/options?resource=posts&record=1&relation=comments&operation=attach2
3
4
5
6
7
Request yang ditolak
| Kondisi | Response |
|---|---|
Tidak ada resource atau field | 422 |
| Resource, relation, atau operation tidak dikenal | 404 |
| Field tidak dideklarasikan schema | 404 |
Field bukan Select | 400 |
User tidak boleh create (canCreate()) | 403 |
Edit form tidak memiliki record | 422 |
User tidak boleh edit (canEdit($record)) | 403 |
User tidak boleh membaca relation (RelationManager::canViewAny($owner)) | 403 |
| User tidak boleh melakukan operation | 403 |
User tidak boleh melihat owner record (canView()) | 403 |
Field di-resolve dari schema yang mendeklarasikannya. Karena itu request hanya dapat mencari field yang memang ada pada form yang sudah boleh dibuka user. Request tidak pernah menyebut column, table, atau model secara langsung.
Attach select
Field related pada relation form—Select yang memilih record yang akan di-attach atau associate—tidak memiliki backing column. Options-nya dihasilkan langsung dari relation:
RelationManager::attachableOptions(Model $owner, ?string $search = null, int $limit = 50): arrayController memanggilnya dengan hard limit 50 yang tidak dapat dinaikkan oleh request. Default implementation mengecualikan record yang sudah berada di relation. Lihat Attach and detach.
Behavior browser
resources/js/panel/forms/fields/SelectField.vue menampilkan search box ketika field searchable dan form memiliki options URL.
- Input search menggunakan debounce 250 ms sebelum request.
- Search term kosong membersihkan search result lokal alih-alih meminta unfiltered page yang sebenarnya sudah dimiliki field.
- Response untuk term lama yang sudah bukan current input dibuang.
- Request gagal membiarkan list sebelumnya tetap ada. Mengubahnya menjadi empty list akan salah diartikan user sebagai “tidak ada hasil”.
- Selected options selalu dipertahankan pada list, apa pun hasil search. Tanpa ini, memilih item lalu mengetik search lain dapat membuat label selected item hilang.
Serialized field membawa options, searchable, multiple, dan usesRelationship.
Tempat endpoint tersedia
optionsUrl diberikan pada resource create/edit pages dan relation forms. Form di context lain—misalnya widget filter—tidak memiliki options URL. Searchable Select pada context tersebut hanya menampilkan options yang sudah diberikan saat render.
Catatan
searchable()ditujukan untuk relation-backed Select. Static option list mengabaikan search term dan mengembalikan list yang sama, sehingga search box hanya menambah round trip tanpa perubahan. Untuk static list, biarkan seluruh options dirender langsung.optionLimit()membatasi initial page dan search result. Term yang cocok dengan ratusan row tetap mengembalikan maksimumoptionLimit().- Relation divalidasi dengan
exists, static list denganin. Memvalidasi relation hanya terhadap rendered page akan menolak key yang sebenarnya valid hanya karena row tersebut berada di urutan berikutnya. - Relation tanpa table yang dapat di-resolve tidak menambahkan rule.
Selectdenganrelationship()tetapi schema tidak memiliki model tidak mengarang validation rule dari UI yang kebetulan ter-render. - Title attribute juga menjadi sort dan search column.
relationship('author', 'name')berarti order dan search menggunakanname. Tidak ada hook ordering terpisah. - Options selalu string di wire.
valuedanlabeldiserialisasi sebagai string. Inilah alasan kondisi frontend juga membandingkan sebagai string. - Many-to-many Select mengambil current value dari pivot, bukan attribute model.
FormSchema::toArray()menggunakanrelatedKeys()sebelum serialization.