Repeatable Entry
PandaPanel\Infolists\Components\RepeatableEntry merender satu sub-schema untuk setiap item. Gunakan ini ketika sebuah record memiliki daftar — misalnya record dari relation atau row di dalam kolom JSON — dan setiap item membutuhkan lebih dari satu nilai untuk ditampilkan.
Child di dalamnya tetap entry biasa. Perilakunya tidak berubah hanya karena berada di dalam repeatable; inilah yang mencegah repeatable menjadi cara kedua untuk mendeskripsikan sebuah nilai.
Repeatable minimal
use PandaPanel\Infolists\Components\DateTimeEntry;
use PandaPanel\Infolists\Components\RepeatableEntry;
use PandaPanel\Infolists\Components\TextEntry;
use PandaPanel\Infolists\Layouts\Grid;
use PandaPanel\Infolists\Layouts\Section;
return $schema->schema([
Section::make('Lines')->schema([
RepeatableEntry::make('lines')
->itemLabel('Line')
->placeholder('No lines on this order.')
->schema([
Grid::make(3)->schema([
TextEntry::make('product'),
TextEntry::make('quantity'),
DateTimeEntry::make('shipped_at')->placeholder('Not shipped'),
]),
]),
]),
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Setiap item digambar di dalam box ber-border sendiri, dengan label bernomor di atasnya.
API
RepeatableEntry mewarisi Entry, sehingga label(), placeholder(), helperText(), columnSpan(), columnSpanFull(), formatUsing(), visible(), dan action() tetap tersedia. Tipe ini menambahkan tiga kemampuan utama:
| Method | Signature | Default |
|---|---|---|
schema() | schema(array $components): self | [] — sub-schema yang dirender untuk setiap item |
columns() | columns(int $columns): self | 1, di-clamp ke 1–4 |
itemLabel() | itemLabel(string $label): self | null — tanpa heading per item |
entries() | entries(): list<Entry> | [$this] — repeatable itu sendiri, bukan child-nya |
toValue() | toValue(Model $record): list<array{label, schema}> | [] jika nilai bukan daftar |
type() | type(): EntryType | EntryType::Repeatable |
schema()
Menerima apa pun yang dapat dimuat infolist — entry, section, grid, dan repeatable nested:
RepeatableEntry::make('passkeys')
->schema([
TextEntry::make('name'),
DateTimeEntry::make('last_used_at')->since()->placeholder('Never'),
]);2
3
4
5
toArray() setiap child dipanggil dengan item sebagai record-nya. Karena itu TextEntry::make('name') membaca nama milik item, bukan nama pada parent record.
columns()
Grid tempat child pada setiap item disusun:
RepeatableEntry::make('lines')->columns(3)->schema([
TextEntry::make('product'),
TextEntry::make('quantity'),
TextEntry::make('total'),
]);2
3
4
5
Jumlah kolom ini diteruskan sebagai lebar container, sehingga columnSpan() milik child dihitung terhadap kolom tersebut. Grid di dalam schema dapat override jumlah kolom untuk child-nya sendiri; pola ini umum digunakan ketika satu baris pada item membutuhkan susunan berbeda dari baris lain.
itemLabel()
Heading yang dipakai setiap item, dinomori berdasarkan posisinya:
RepeatableEntry::make('lines')->itemLabel('Line');
// 'Line 1', 'Line 2', 'Line 3'2
Nomor dibentuk melalui sprintf('%s %d', $label, $index + 1) — yaitu posisi dalam daftar hasil render, bukan database key. Tanpa itemLabel(), label milik item bernilai null dan heading tidak ditampilkan.
Apa yang dianggap sebagai item
toValue() me-resolve attribute terlebih dahulu — melalui formatUsing() jika ada — lalu membuka collection dan hanya mempertahankan nilai yang dapat dibaca oleh child entry:
| Nilai | Hasil |
|---|---|
Illuminate\Database\Eloquent\Collection | Dibuka dengan all(), lalu diproses seperti di bawah |
Illuminate\Support\Collection | Dibuka dengan all(), lalu diproses seperti di bawah |
array berisi Model | Digunakan langsung |
array berisi array | Setiap row dibungkus menjadi InfolistRow |
| Selain itu (string, int, null) | Dibuang — toValue() mengembalikan [] |
Karena itu kedua bentuk berikut dapat menggunakan child schema yang sama:
// A relation.
RepeatableEntry::make('passkeys')->schema([TextEntry::make('name')]);
// A JSON column holding [['title' => 'First'], ['title' => 'Second']].
RepeatableEntry::make('lines')->schema([TextEntry::make('title')]);2
3
4
5
Scalar yang tercampur di dalam daftar dibuang, bukan dirender. Memberikan string kepada child yang mengharapkan record hanya akan menghasilkan item penuh em dash tanpa penjelasan.
InfolistRow
Row yang bukan model dibungkus dalam PandaPanel\Infolists\Support\InfolistRow, yaitu Model tanpa table, tanpa timestamp, dan tanpa guarded attribute:
use PandaPanel\Infolists\Support\InfolistRow;
$row = InfolistRow::wrap(['title' => 'First', 'quantity' => 2]);
$row->getAttribute('title'); // 'First'
$row->exists; // false2
3
4
5
Wrapper ini ada agar child selalu menerima sebuah model. Alternatifnya adalah memperlebar seluruh entry, closure, dan signature infolist menjadi Model|array, tetapi itu akan merusak semua callback seperti formatUsing(fn (mixed $value, Model $record) => …) hanya demi kasus yang eksklusif ada di satu komponen.
Key dicast menjadi string dan row diisi menggunakan forceFill() — attribute ini berasal dari data internal panel, bukan request input, dan model tersebut tidak memiliki table yang perlu dilindungi. Row tidak pernah disimpan atau di-query dan hanya hidup selama satu proses toArray().
Repeatable dihitung sebagai satu entry
$entry = RepeatableEntry::make('lines')->schema([TextEntry::make('title')]);
$entry->entries(); // [$entry]2
3
Child milik item, bukan milik record utama. Jika child dihitung sebagai entry record, schema akan mengklaim ada nilai top-level yang sebenarnya tidak ada. InfolistSchema::entries() dan panelInfolistLabels() sama-sama melihat repeatable sebagai satu entry lalu berhenti menelusuri lebih dalam.
Tidak ada action pada item
Entry di dalam repeatable tidak membawa action. Entry::toArray() memeriksa $record->exists sebelum menyerialisasi action, sedangkan wrapped row tidak memiliki key:
$entry = RepeatableEntry::make('lines')->schema([
TextEntry::make('title')->action(Action::make('rename')),
]);
$value = $entry->toValue($record);
$value[0]['schema'][0]['action']; // null2
3
4
5
6
Action yang menunjuk ke row seperti itu akan menamai record yang tidak dapat ditemukan endpoint. Hal yang sama berlaku untuk Section::headerActions() di dalam repeatable.
Repeatable yang berbasis relation berisi model nyata yang sudah disimpan, sehingga $record->exists bernilai true dan action pada child dapat diserialisasi. Namun endpoint infolist me-resolve record melalui Resource::findRecord(), yang mencari model milik resource utama. Action terhadap related record adalah tanggung jawab relation manager. Lihat Relation managers.
Data yang dikirim ke frontend
[
'component' => 'entry',
'name' => 'lines',
'label' => 'Lines',
'type' => 'repeatable',
'columns' => 3,
'value' => [
['label' => 'Line 1', 'schema' => [/* the children, serialized against item 1 */]],
['label' => 'Line 2', 'schema' => [/* item 2 */]],
],
'placeholder' => 'No lines on this order.',
'helperText' => null,
'columnSpan' => 1,
'action' => null,
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
Child dikirim kembali melalui InfolistNode.vue, renderer yang sama dengan top-level tree. Karena itu Grid di dalam repeatable berperilaku persis seperti grid di tempat lain. Jika value kosong, renderer menampilkan placeholder.
Testing
toValue() hanya membutuhkan sebuah record:
use PandaPanel\Infolists\Components\RepeatableEntry;
use PandaPanel\Infolists\Components\TextEntry;
it('renders one item per line', function (): void {
$entry = RepeatableEntry::make('lines')
->itemLabel('Line')
->schema([TextEntry::make('title')]);
$value = $entry->toValue(new Order(['lines' => [
['title' => 'First'],
['title' => 'Second'],
]]));
expect($value)->toHaveCount(2)
->and($value[0]['label'])->toBe('Line 1')
->and($value[0]['schema'][0]['value'])->toBe('First');
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Catatan
- Eager-load relation. Repeatable untuk
passkeysmemang membaca relation satu kali, tetapi entry lain yang menyentuh relation itu juga akan membacanya.protected static array $with = ['passkeys'];pada resource menjaga halaman view tetap memakai satu query untuk relation tersebut.Model::shouldBeStrict()di luar production juga membantu mengubah eager-load yang terlupa menjadi failure saat development, bukan biaya diam-diam. - Tidak ada pagination, limit, atau ordering. Repeatable merender semua item yang dimiliki nilai, sesuai urutan nilai tersebut. Atur order di relation atau melalui
formatUsing()jika urutan penting. formatUsing()menyediakan daftar item. Callback menerima raw attribute dan harus mengembalikan model, row, atau collection dari keduanya. Mengembalikan string yang sudah diformat akan menghasilkan repeatable kosong.- Nested repeatable didukung tetapi jarang menjadi pilihan yang baik. Item di dalam item biasanya sulit dipindai. Dua section atau relation manager biasanya menyampaikan struktur dengan lebih jelas.
- Placeholder digunakan untuk "tidak ada item" dan "nilai bukan list". Keduanya menghasilkan
[], sehingga typo pada nama attribute terlihat sama dengan relation yang memang kosong.