Live Fields
Field yang diberi live() meminta server membangun ulang schema setelah value-nya berubah. Gunakan fitur ini ketika satu field bergantung pada field lain dengan cara yang tidak dapat diekspresikan oleh declarative conditions—misalnya options sebuah Select berasal dari value Select lain, total dihitung dari beberapa input, atau sebuah Section baru muncul setelah user memilih type tertentu. Fitur ini default-nya mati karena round trip ke server pada setiap keystroke bukan default yang baik.
Contoh minimal
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\FormSchema;
public static function form(FormSchema $schema): FormSchema
{
$country = request()->input('state.country');
return $schema->schema([
Select::make('country')
->options(['id' => 'Indonesia', 'sg' => 'Singapore'])
->live(),
Select::make('region')->options(match ($country) {
'id' => ['jkt' => 'Jakarta', 'bdg' => 'Bandung'],
'sg' => ['central' => 'Central', 'east' => 'East'],
default => [],
}),
TextInput::make('note')->live(onBlur: true, debounce: 1000),
]);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Saat country berubah, current form values dikirim ke endpoint form-state. Method form() dijalankan ulang dengan state tersebut tersedia pada request, lalu schema baru menggantikan schema lama di UI. Value yang sudah diketik user tetap dipertahankan.
live()
public function live(bool $onBlur = false, ?int $debounce = null): static| Argument | Default | Efek |
|---|---|---|
$onBlur | false | Menunggu focus keluar dari control alih-alih menggunakan debounce |
$debounce | 500 ms | Waktu tunggu setelah perubahan terakhir sebelum request dikirim |
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\Components\TextInput;
Select::make('kind')->live(); // debounce 500 ms
TextInput::make('slug')->live(debounce: 1000); // lebih lambat
TextInput::make('vat_number')->live(onBlur: true); // hanya saat focus keluar2
3
4
5
6
Field::isLive(): bool mengembalikan status tersebut. Serialized field membawa live sebagai ['onBlur' => bool, 'debounce' => int], atau null bila field tidak live. Frontend menggunakan informasi ini untuk mengetahui apakah perlu melakukan request.
Select::make('kind')->live(onBlur: true, debounce: 250)->toArray(null, 'create')['live'];
// ['onBlur' => true, 'debounce' => 250]2
afterStateUpdated()
public function afterStateUpdated(Closure $callback): static
// Closure(mixed $new, mixed $old, ?Model $record): void2
Hook ini berjalan di server ketika value dari live field berubah, sebelum schema dibangun ulang. Gunakan untuk side effect atau untuk memengaruhi bagaimana field lain akan dibangun. Hook tidak mengembalikan value karena jika hook sekaligus mutate dan return, akan ada dua tempat berbeda yang dapat mengubah state.
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Forms\Components\Select;
Select::make('plan')
->options(['free' => 'Free', 'pro' => 'Pro'])
->live()
->afterStateUpdated(static function (mixed $new, mixed $old, ?Model $record): void {
logger()->info('plan changed', ['from' => $old, 'to' => $new]);
});2
3
4
5
6
7
8
9
Hook hanya dijalankan untuk field yang memang mendeklarasikan live(), apa pun yang diklaim request sebagai field yang berubah. Field::handleStateUpdated(mixed $state, mixed $previous, ?Model $record = null): void dapat dipanggil langsung untuk testing:
$field = TextInput::make('a')->live()->afterStateUpdated($hook);
$field->handleStateUpdated('new', 'old');2
3
Endpoint
Route bernama panel.{panel_id}.form-state, diregistrasikan per Panel, dan ditangani oleh PandaPanel\Http\Controllers\PanelFormStateController.
| Bagian | Sumber | Value |
|---|---|---|
resource | query string | slug resource pada Panel; 422 jika tidak ada, 404 jika tidak dikenal |
page | query string | edit, atau create untuk value lainnya |
record | query string | wajib saat page=edit; 422 jika tidak ada, 404 jika record tidak ditemukan |
state | request body | seluruh value yang telah diketik, keyed by field name |
changed | request body | nama field yang berubah |
previous | request body | value sebelumnya, diteruskan ke hook |
URL dibangun di server oleh PandaPanel\Support\FormEndpoints::formState() lalu dikirim ke Page sebagai formStateUrl. Semua informasi yang menentukan form mana yang sedang diminta berada pada URL. Browser hanya mengirim current values dan field yang berubah, sehingga keystroke tidak dapat mengubah identitas form yang sedang diminta.
Response berupa JSON:
{ "form": { "columns": 2, "schema": [ { "component": "field", "name": "region", "…": "…" } ] } }Response tersebut adalah hasil FormSchema::toArrayWithState($record, $state): schema dibangun ulang kemudian submitted values diterapkan kembali ke field.
Yang tidak dilakukan endpoint
- Tidak menjalankan validation. Rules tidak dieksekusi dan tidak ada validation error yang dikembalikan.
- Tidak menulis data. Meminta bentuk schema bukan form submission.
- Tetap melakukan authorization. Membangun create form memerlukan
canCreate(), sedangkan edit form memerlukancanEdit($record). Authorization dilakukan sebelum schema dibangun karena proses membangun schema dapat menjalankan Closure milik application. - State dinarrow ke field yang dideklarasikan. Key lain di dalam
statedibuang sebelum hook mana pun membacanya.
Pemisahan tersebut membuat endpoint aman dipanggil sesering perubahan live field. Crafted request paling jauh hanya dapat meminta deskripsi form yang memang sudah dapat dilihat dengan membuka Page tersebut.
Behavior browser
Timing dimiliki resources/js/panel/forms/FormRenderer.vue.
- Perubahan pada live field membuat timer
debouncemilik field tersebut. Timer sebelumnya untuk field yang sama digantikan. - Dengan
onBlur, timer tidak dibuat. Request dikirim saat focus keluar dari control. Setiap control menggunakan field name sebagai DOMid, sehingga satu listenerfocusoutpada form dapat menangani semuanya. - Hanya satu request yang dibiarkan in-flight. Request baru meng-abort request sebelumnya agar response lama yang datang terlambat tidak mengembalikan form ke state beberapa keystroke sebelumnya.
- Saat berhasil, schema diganti. Field baru memperoleh serialized value dari schema baru. Field yang sudah diisi user tetap mempertahankan current value.
- Jika request gagal, form dibiarkan persis seperti sebelumnya. Schema rebuild merupakan enrichment; kegagalan rebuild tidak boleh menghapus input user.
- Response dengan shape yang tidak sesuai diperlakukan sebagai failure, bukan dipaksa masuk dengan type assertion.
Membaca submitted state dari form()
Endpoint menjalankan ulang Resource::form(), sehingga semua state dapat dibaca melalui request pada key state:
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\FormSchema;
public static function form(FormSchema $schema): FormSchema
{
/** @var array<string, mixed> $state */
$state = (array) request()->input('state', []);
return $schema->schema([
Select::make('category')->relationship('category', 'name')->live(),
Select::make('subcategory')->options(
Subcategory::query()
->where('category_id', $state['category'] ?? null)
->pluck('name', 'id')
->all(),
),
]);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Pada initial render belum ada state, sehingga dependent field menggunakan fallback yang Anda definisikan. Live rebuild menjalankan code path yang sama. Dengan begitu initial render dan rebuild tidak memiliki dua implementasi yang dapat berbeda behavior.
Tempat live field bekerja
formStateUrl hanya diberikan oleh resource create dan edit pages. Context lain tidak menyediakan endpoint ini. Karena itu field live() di dalam action dialog, relation form, atau widget filter berperilaku seperti ordinary field: tidak ada request, tidak ada rebuild, dan afterStateUpdated() tidak dijalankan.
Catatan
- Gunakan declarative condition jika cukup.
visibleWhen()danhiddenWhen()dievaluasi ulang langsung di browser tanpa request.live()digunakan hanya untuk dependensi yang tidak dapat diekspresikan oleh kondisi tersebut. Lihat Field visibility. - Rebuild mengganti schema, bukan working values. Jika sebuah field dihapus dari schema baru, value lama masih dapat tersimpan pada working state browser sampai submit. Saat submit, value tersebut dibuang karena field tidak lagi dideklarasikan.
afterStateUpdated()tidak pernah berjalan pada submit biasa. Hook ini hanya bagian dari endpoint live state.previousberasal dari browser. Value ini adalah current browser value sebelum perubahan yang memicu request. Perlakukan sebagai untrusted input seperti request data lainnya.- Live field di dalam Wizard tetap membangun ulang seluruh form. Response berisi complete schema termasuk seluruh steps.
- Biaya request dihitung per live field, bukan per form. Form dengan satu live Select hanya mengirim request saat Select tersebut berubah; field lainnya tidak menambah request.