Visibility Field
Sebuah field menjawab empat pertanyaan berbeda tentang apakah ia ditampilkan: field berada di Page mana, record apa yang sedang diedit, value apa yang sedang dimiliki field lain, dan apakah field dapat diedit. Keempatnya sengaja dipisahkan karena tiga keputusan dibuat satu kali di server, sedangkan satu keputusan harus dievaluasi ulang di browser setiap kali user mengetik. Gunakan halaman ini ketika field hanya boleh ada pada create atau edit, ketika visibility bergantung pada record, atau ketika field baru boleh muncul setelah field lain memiliki value tertentu.
Contoh minimal
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\Enums\ConditionOperator;
use PandaPanel\Forms\FormSchema;
FormSchema::make()->schema([
TextInput::make('slug')->visibleOn(['edit']), // Page mana
TextInput::make('email')->disabledOn(['edit']), // tampil, tetapi tidak editable
TextInput::make('reason')->visible( // record
static fn (?Model $record): bool => $record !== null,
),
Select::make('kind')->options(['plain' => 'Plain', 'other' => 'Other']),
TextInput::make('other') // value field lain
->visibleWhen('kind', ConditionOperator::Equals, 'other'),
]);2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Tiga kondisi pertama diputuskan ketika schema dibangun. Kondisi terakhir dievaluasi di browser setiap keystroke tanpa request ke server.
Arti sebenarnya dari “hidden”
Hidden field bukan sekadar tidak terlihat. FormSchema::fields() memfilter berdasarkan Field::isHiddenOn(), dan seluruh proses schema diturunkan dari daftar field tersebut:
Turunan dari fields() | Dampak bagi hidden field |
|---|---|
toArray() | tidak diserialisasi, sehingga browser tidak pernah menerima field |
validationRules() | tidak memiliki rule, sehingga tidak ada validation message untuk field tersebut |
dehydrate() | tidak menghasilkan attribute, sehingga tidak ada value yang ditulis |
Crafted request yang mengirim slug ke create page ketika schema menyembunyikan slug tidak dapat membuat field tersebut menjadi valid. Key dibuang sebelum dipakai, sama seperti key yang tidak pernah dideklarasikan schema.
Visibility berdasarkan Page
Setiap schema dibangun untuk satu Page melalui FormSchema::forPage() dan dapat dibaca dengan getPage(). Page keys yang digunakan framework:
| Key | Sumber |
|---|---|
create | CreateRecord, CreateAction, relation form untuk related record baru |
edit | EditRecord, relation form yang mengedit related record existing |
view | ViewRecord |
public function visibleOn(array $pages): static
public function hiddenOn(array $pages): static
public function disabledOn(array $pages): static2
3
use PandaPanel\Forms\Components\TextInput;
TextInput::make('slug')->visibleOn(['edit']); // hanya edit
TextInput::make('slug')->hiddenOn(['create']); // semua Page kecuali create
TextInput::make('email')->disabledOn(['edit', 'view']);2
3
4
5
visibleOn() dan hiddenOn() adalah kebalikan satu sama lain. Keduanya disediakan agar declaration dapat dibaca sesuai intent. Field yang hanya untuk create lebih jelas sebagai visibleOn(['create']) daripada daftar semua Page lain. Default visibleOn() adalah null, yang berarti “tidak ada pembatasan Page”. Ini berbeda dari visibleOn([]), yang menyembunyikan field pada semua Page.
Page juga tersedia ketika schema sedang dibangun. Ini merupakan cara umum untuk memberi konfigurasi berbeda per Page:
use PandaPanel\Forms\Components\PasswordInput;
PasswordInput::make('password')
->when(
$schema->getPage() === 'create',
static fn (PasswordInput $field): PasswordInput => $field->required(),
static fn (PasswordInput $field): PasswordInput => $field->optionalWhenFilled(),
);2
3
4
5
6
7
8
when() dan unless() berasal dari Illuminate\Support\Traits\Conditionable milik Laravel yang digunakan Field. Keduanya merupakan configuration biasa dan dievaluasi saat schema dibangun.
Visibility berdasarkan record
public function hidden(Closure|bool $condition = true): static
public function visible(Closure|bool $condition = true): static2
Keduanya menerima boolean atau Closure(?Model $record): bool. Pada create page, record bernilai null. Hal ini membuat kondisi seperti “hanya saat edit” dapat diekspresikan langsung:
use Illuminate\Database\Eloquent\Model;
use PandaPanel\Forms\Components\Textarea;
Textarea::make('rejection_reason')->visible(
static fn (?Model $record): bool => $record?->isRejected() === true,
);
Textarea::make('internal_note')->hidden(
static fn (?Model $record): bool => $record === null,
);
Textarea::make('legacy_body')->hidden(); // selalu hidden2
3
4
5
6
7
8
9
10
11
12
Closure berjalan satu kali ketika schema diserialisasi. Closure tersebut tidak dapat bereaksi terhadap value yang sedang diketik. Untuk dependency yang harus reaktif, gunakan declarative conditions pada bagian berikut. Pemisahan ini disengaja agar jelas mana condition yang server-side dan mana yang client-side.
Urutan visibility check
Field::isHiddenOn() memeriksa empat sumber dengan prinsip kondisi paling ketat menang:
public function isHiddenOn(string $page, ?Model $record = null): boolhidden()eksplisit mengembalikan true.visible()mengembalikan false.visibleOn()diset tetapi Page saat ini tidak termasuk daftar.hiddenOn()menyebut Page saat ini.
Karena itu field yang memiliki visible(fn () => false) sekaligus visibleOn(['edit']) tetap hidden pada edit. Tidak ada later check yang dapat menampilkan kembali field yang sudah disembunyikan earlier check.
Disabled berbeda dengan hidden
public function disabled(bool $disabled = true): static
public function disabledOn(array $pages): static
public function isDisabledOn(string $page, ?Model $record = null): bool2
3
Field disabled tetap dirender, tetap menampilkan value, dan tetap berada di validation rules. Disabled adalah presentation state, bukan absence:
use PandaPanel\Forms\Components\TextInput;
TextInput::make('email')->disabled();
TextInput::make('email')->disabledOn(['edit']);2
3
4
disabled() hanya menerima boolean. Tidak ada record-aware callback untuk disabled. Pertanyaan record-aware pada Field adalah hidden() dan visible(). Jika field harus read-only pada beberapa record tetapi editable pada record lain, sembunyikan field lalu tampilkan alternatif read-only, atau lakukan branching saat membangun schema.
Karena disabled merupakan browser control state, ia bukan write guard. Value tetap dapat dikirim melalui modified request, tetap divalidasi, dan tetap di-dehydrate. Gunakan dehydrated(false) jika field sama sekali tidak boleh ditulis apa pun yang dikirim browser.
Kondisi berdasarkan value field lain
public function visibleWhen(
string $field,
ConditionOperator $operator = ConditionOperator::Truthy,
mixed $value = null,
): static
public function hiddenWhen(
string $field,
ConditionOperator $operator = ConditionOperator::Truthy,
mixed $value = null,
): static2
3
4
5
6
7
8
9
10
11
Kedua method ini berbeda secara fundamental dari visibility sebelumnya. Kondisi harus bereaksi saat user mengetik, sehingga evaluasinya dilakukan di browser. Namun tidak ada executable PHP atau JavaScript yang dikirim dari server. Server hanya mengirim deskripsi comparison, lalu frontend yang sudah di-compile menjalankannya.
use PandaPanel\Forms\Components\Select;
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\Components\Toggle;
use PandaPanel\Forms\Enums\ConditionOperator;
Select::make('kind')->options(['plain' => 'Plain', 'other' => 'Other']),
TextInput::make('other_kind')
->visibleWhen('kind', ConditionOperator::Equals, 'other'),
Toggle::make('notify'),
TextInput::make('notify_email')
->visibleWhen('notify'), // Truthy, default
TextInput::make('override')
->hiddenWhen('locked', ConditionOperator::Truthy),2
3
4
5
6
7
8
9
10
11
12
13
14
15
Setiap pemanggilan menambahkan condition baru. Beberapa visibleWhen() digabung menggunakan AND, sementara satu hiddenWhen() yang match akan tetap menyembunyikan field:
TextInput::make('other')
->visibleWhen('kind', ConditionOperator::Filled)
->visibleWhen('quantity', ConditionOperator::GreaterThan, 0)
->hiddenWhen('locked', ConditionOperator::Truthy);2
3
4
Operator condition
PandaPanel\Forms\Enums\ConditionOperator adalah closed set. Kondisi yang tidak dapat diekspresikan di sini seharusnya menggunakan server-side visible() Closure, yang secara eksplisit hanya dievaluasi saat render.
| Case | Wire value | Membutuhkan value | Match ketika |
|---|---|---|---|
Equals | equals | ya | kedua value sama jika dibandingkan sebagai string |
NotEquals | not_equals | ya | kedua value berbeda |
In | in | ya, array | current value ada di daftar |
NotIn | not_in | ya, array | current value tidak ada di daftar, atau daftar kosong |
Filled | filled | tidak | bukan null, '', atau [] |
Blank | blank | tidak | null, '', atau [] |
GreaterThan | greater_than | ya | kedua sisi numeric dan kiri > kanan |
LessThan | less_than | ya | kedua sisi numeric dan kiri < kanan |
Truthy | truthy | tidak | mengikuti PHP truthiness; '0' dianggap false |
Falsy | falsy | tidak | kebalikan dari Truthy |
public function needsValue(): bool
public function matches(mixed $state, mixed $expected): bool2
use PandaPanel\Forms\Enums\ConditionOperator;
ConditionOperator::In->matches('b', ['a', 'b']); // true
ConditionOperator::GreaterThan->matches('5', 3); // true
ConditionOperator::GreaterThan->matches('abc', 3); // false — tidak dapat dibandingkan sebagai numeric
ConditionOperator::Filled->needsValue(); // false2
3
4
5
6
Comparison dilakukan sebagai string untuk operator equality. Karena itu integer 1 dari model dan string '1' dari form dianggap sama. Form values memang melewati wire sebagai text untuk banyak control, apa pun type column aslinya.
Object Condition
visibleWhen() dan hiddenWhen() membangun value object PandaPanel\Forms\Support\Condition. Biasanya Anda tidak perlu membuatnya sendiri, tetapi class ini public dan merupakan bentuk yang diserialisasi:
final readonly class Condition
{
public function __construct(
public string $field,
public ConditionOperator $operator,
public mixed $value = null,
) {}
public static function make(
string $field,
ConditionOperator $operator = ConditionOperator::Truthy,
mixed $value = null,
): self;
/** @param array<string, mixed> $state */
public function matches(array $state): bool;
/** @return array{field: string, operator: string, value: mixed} */
public function toArray(): array;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use PandaPanel\Forms\Enums\ConditionOperator;
use PandaPanel\Forms\Support\Condition;
Condition::make('kind', ConditionOperator::Equals, 'special')
->matches(['kind' => 'special']); // true
// Operator yang tidak membutuhkan comparison value mengirim value null.
Condition::make('kind', ConditionOperator::Filled, 'ignored')->toArray();
// ['field' => 'kind', 'operator' => 'filled', 'value' => null]2
3
4
5
6
7
8
9
Payload yang diterima browser
Setiap serialized field membawa key conditions:
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\Enums\ConditionOperator;
TextInput::make('other')
->visibleWhen('kind', ConditionOperator::Equals, 'special')
->toArray(null, 'create')['conditions'];2
3
4
5
6
{
"visibleWhen": [{ "field": "kind", "operator": "equals", "value": "special" }],
"hiddenWhen": []
}2
3
4
resources/js/panel/forms/conditions.ts adalah implementation frontend yang sudah di-compile. Behavior-nya dibuat mirror terhadap enum PHP, termasuk truthiness PHP untuk '0', dan mengekspor:
export function matchesConditions(
conditions: FieldConditions | undefined,
values: FormValues,
): boolean;
export function conditionDependencies(
conditions: FieldConditions | undefined,
): string[];2
3
4
5
6
7
8
FormComponentRenderer.vue tidak merender field jika conditions tidak terpenuhi. validateFields() juga melewatinya pada client-side pre-check.
Mengevaluasi condition di PHP
/** @param array<string, mixed> $state */
public function matchesConditions(array $state): bool2
Object yang sama dapat dievaluasi di server sehingga test dapat memastikan behavior condition sesuai ekspektasi:
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\Enums\ConditionOperator;
$field = TextInput::make('other')
->visibleWhen('kind', ConditionOperator::Equals, 'special');
$field->matchesConditions(['kind' => 'special']); // true
$field->matchesConditions(['kind' => 'plain']); // false2
3
4
5
6
7
8
Method ini hanya mengevaluasi declarative conditions. Server-side visibility sudah diputuskan sebelum schema diserialisasi. Field yang disembunyikan oleh hidden()/visible() tidak masuk payload sama sekali.
Referensi method
| Method | Signature | Dievaluasi |
|---|---|---|
visibleOn() | (list<string> $pages): static | server, saat build |
hiddenOn() | (list<string> $pages): static | server, saat build |
disabledOn() | (list<string> $pages): static | server, saat build |
visible() | (Closure(?Model): bool|bool $condition = true): static | server, satu kali per render |
hidden() | (Closure(?Model): bool|bool $condition = true): static | server, satu kali per render |
disabled() | (bool $disabled = true): static | server, saat build |
visibleWhen() | (string $field, ConditionOperator $operator = Truthy, mixed $value = null): static | browser, setiap perubahan state |
hiddenWhen() | (string $field, ConditionOperator $operator = Truthy, mixed $value = null): static | browser, setiap perubahan state |
isHiddenOn() | (string $page, ?Model $record = null): bool | reader |
isDisabledOn() | (string $page, ?Model $record = null): bool | reader |
matchesConditions() | (array<string, mixed> $state): bool | reader/test |
Gotchas
Field yang hidden hanya karena declarative condition tetap berada di server rule set. FormSchema::validationRules() dibangun dari isHiddenOn(), dan method tersebut tidak membaca visibleWhen()/hiddenWhen(). Karena itu:
->required()->visibleWhen('kind', Equals, 'other')masih menghasilkan server rule required meskipun browser sedang tidak menggambar field. Form dapat ditolak karena field yang tidak terlihat. Ekspresikan condition yang sama pada server menggunakan Laravel conditional rules:
use PandaPanel\Forms\Components\TextInput;
use PandaPanel\Forms\Enums\ConditionOperator;
TextInput::make('other_kind')
->visibleWhen('kind', ConditionOperator::Equals, 'other')
->rules(['required_if:kind,other']);2
3
4
5
6
Condition menyebut field name, bukan arbitrary path. Lookup menggunakan flat form value map dan nama dari getName(). Field di dalam Relationship sudah membawa relation prefix seperti profile.bio. Di dalam Repeater atau Builder block, condition membaca value item tersebut menggunakan nama plain dari sub-schema.
Value field yang disembunyikan client-side tetap ikut submit. Browser mempertahankan working value field walaupun condition membuatnya tidak dirender. Server rules dan dehydrate() yang menentukan apakah value akhirnya diterima/ditulis. Frontend sengaja tidak menghapus value karena client tidak boleh menjadi authority atas bentuk form.
visibleOn([]) menyembunyikan field di semua Page. Empty list berarti tidak ada Page yang diizinkan. Untuk “tidak ada restriction”, jangan memanggil visibleOn().
disabled() bukan write guard. Gunakan dehydrated(false) untuk field yang tidak boleh mencapai column, dan hidden() untuk field yang tidak boleh ada pada form sama sekali.
Condition tidak dapat membaca owner Relation Manager atau record secara langsung. Declarative condition hanya membaca values milik form. Gunakan visible()/hidden() untuk record-dependent visibility.
Lihat juga
- Forms and Schemas — schema tempat field berada
- Disabled and Hidden Fields
- Validation — cara server rule set dibangun
- Live Fields — untuk dependency yang tidak dapat diekspresikan condition
- State Lifecycle —
dehydrated(),dehydrateTo(), dan hooks - Layouts — Section, Grid, Tabs, dan Wizard
- Resource Pages — sumber Page key