Tanggal dan Waktu
Ada tiga field terpisah, bukan satu field dengan banyak flag: PandaPanel\Forms\Components\DatePicker, PandaPanel\Forms\Components\DateTimePicker, dan PandaPanel\Forms\Components\TimePicker. Ketiganya memformat value secara berbeda, menggunakan validation berbeda, dan merender native control yang berbeda. Jika semuanya dipaksakan ke satu class, hampir setiap behavior akan menjadi branch. Gunakan field yang bentuk value-nya sesuai dengan column yang akan disimpan.
Contoh minimal
use PandaPanel\Forms\Components\DatePicker;
use PandaPanel\Forms\Components\DateTimePicker;
use PandaPanel\Forms\Components\TimePicker;
use PandaPanel\Forms\FormSchema;
FormSchema::make()->schema([
DatePicker::make('born_on'),
DateTimePicker::make('published_at'),
TimePicker::make('opens_at'),
]);2
3
4
5
6
7
8
9
10
Cast attribute pada model agar field menerima instance Carbon, bukan raw string:
protected function casts(): array
{
return [
'born_on' => 'date',
'published_at' => 'datetime',
];
}2
3
4
5
6
7
Ringkasan
| Field | FieldType | Control | Format value | Type rules |
|---|---|---|---|---|
DatePicker | Date ('date') | <input type="date"> | Y-m-d | date |
DateTimePicker | DateTime ('datetime') | <input type="datetime-local"> | Y-m-d H:i saat submit | date |
TimePicker | Time ('time') | <input type="time"> | H:i atau H:i:s | date_format:H:i[:s] |
DatePicker
public function minDate(?string $date): self // default: null
public function maxDate(?string $date): self // default: null2
use PandaPanel\Forms\Components\DatePicker;
DatePicker::make('starts_on')
->label('Start date')
->minDate('2020-01-01')
->maxDate('today')
->required();2
3
4
5
6
7
Keduanya default null dan melakukan dua hal sekaligus: value menjadi attribute min / max pada control, serta menjadi Laravel validation rule.
use PandaPanel\Forms\Components\DatePicker;
use PandaPanel\Forms\FormSchema;
FormSchema::make()
->schema([DatePicker::make('starts_on')->minDate('2020-01-01')->maxDate('2030-12-31')])
->validationRules();
// ['starts_on' => ['nullable', 'date', 'after_or_equal:2020-01-01', 'before_or_equal:2030-12-31']]2
3
4
5
6
7
8
String diteruskan apa adanya, sehingga apa pun yang diterima after_or_equal dapat digunakan — termasuk relative string seperti today. Browser tidak memahami today sebagai attribute min, tetapi Laravel tetap menghormatinya saat validation. Mengirim null akan menghapus bound yang sebelumnya sudah diset.
Hydration
protected function castForForm(mixed $value): ?string| Stored value | Value yang dikirim ke control |
|---|---|
CarbonInterface | ->format('Y-m-d') |
| string yang tidak kosong | sepuluh karakter pertama |
| selain itu | null |
Pemotongan menjadi sepuluh karakter membuat column datetime yang belum di-cast tetap dapat digunakan: 2026-08-15 09:30:00 menjadi 2026-08-15. Mengosongkan control mengirim null.
DateTimePicker
public function minDate(?string $date): self // default: null
public function maxDate(?string $date): self // default: null
public function seconds(bool $seconds = true): self // default: false2
3
use PandaPanel\Forms\Components\DateTimePicker;
DateTimePicker::make('published_at')
->label('Publish at')
->minDate('2026-01-01 00:00')
->seconds()
->helperText('Stored and displayed in the application timezone.');2
3
4
5
6
7
seconds() menentukan format value yang dirender — Y-m-d\TH:i:s alih-alih Y-m-d\TH:i — dan mengatur step control menjadi 1. Attribute tersebutlah yang membuat browser menampilkan input detik. Tanpanya browser membulatkan ke menit dan value yang memiliki detik akan terpotong pada edit pertama.
Rules-nya adalah date ditambah pasangan after_or_equal / before_or_equal yang sama seperti DatePicker. seconds() tidak menambah rule karena date menerima kedua tingkat presisi tersebut.
Karakter T pada boundary
PHP memformat value untuk control menggunakan literal T, misalnya 2026-08-15T09:30, karena itulah format yang diwajibkan datetime-local. Control kemudian mengirim kembali value dengan spasi, misalnya 2026-08-15 09:30, karena format itu lebih sesuai untuk disimpan ke column. Konversi dilakukan satu kali di DateTimeField.vue, sehingga kedua sisi dapat memakai format yang paling sesuai untuk masing-masing kebutuhan.
Hal ini penting jika sebuah hook membaca raw submitted value: formatnya Y-m-d H:i atau Y-m-d H:i:s, bukan ISO-8601 dengan T.
Hydration
| Stored value | Value yang dikirim ke control |
|---|---|
CarbonInterface | ->format('Y-m-d\TH:i'), atau ...:s jika seconds() aktif |
| string yang tidak kosong | diteruskan apa adanya |
| selain itu | null |
Berbeda dari DatePicker, raw string diteruskan tanpa perubahan. Column tanpa cast yang mengembalikan 2026-08-15 09:30:00 akan mengirim detik ke control walaupun control mungkin tidak menampilkannya. Cast attribute ke datetime agar formatting ditangani framework.
TimePicker
public function seconds(bool $seconds = true): self // default: falseuse PandaPanel\Forms\Components\TimePicker;
TimePicker::make('opens_at'),
TimePicker::make('cron_at')->seconds(),2
3
4
Field menggunakan strict format validation, bukan date:
use PandaPanel\Forms\Components\TimePicker;
use PandaPanel\Forms\FormSchema;
FormSchema::make()
->schema([TimePicker::make('opens_at'), TimePicker::make('cron_at')->seconds()])
->validationRules();
// [
// 'opens_at' => ['nullable', 'date_format:H:i'],
// 'cron_at' => ['nullable', 'date_format:H:i:s'],
// ]2
3
4
5
6
7
8
9
10
11
date_format bersifat exact. Value yang memiliki detik akan gagal pada field tanpa seconds(), dan value tanpa detik akan gagal pada field yang mengaktifkannya. Ini memang disengaja karena kedua bentuk tersebut bukan format yang dapat dipertukarkan begitu saja.
Hydration
| Stored value | Value yang dikirim ke control |
|---|---|
CarbonInterface | ->format('H:i'), atau 'H:i:s' jika seconds() aktif |
| string yang tidak kosong | diteruskan apa adanya |
| selain itu | null |
Data yang dikirim ke frontend
interface DateFieldDefinition extends BaseFieldDefinition {
type: 'date';
minDate: string | null;
maxDate: string | null;
}
interface DateTimeFieldDefinition extends BaseFieldDefinition {
type: 'datetime';
minDate: string | null;
maxDate: string | null;
seconds: boolean;
}
interface TimeFieldDefinition extends BaseFieldDefinition {
type: 'time';
seconds: boolean;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Ketiga control mengirim null untuk input kosong, bukan '', sehingga field tanggal optional dapat dibersihkan dengan benar ke nullable column.
Resep penggunaan
End date tidak boleh lebih awal daripada start date. minDate() menerima rule string sehingga secara teori dapat menunjuk field lain, tetapi value itu juga menjadi attribute min pada browser, dan nama field tidak memiliki arti di sana. Gunakan rule secara langsung:
use PandaPanel\Forms\Components\DatePicker;
DatePicker::make('starts_on')->required(),
DatePicker::make('ends_on')->rules(['after_or_equal:starts_on']),2
3
4
Timestamp tidak boleh berada di masa lalu saat create. when() berasal dari Conditionable, dan page tersedia pada schema:
use PandaPanel\Forms\Components\DateTimePicker;
DateTimePicker::make('scheduled_for')
->when(
$schema->getPage() === 'create',
static fn (DateTimePicker $field): DateTimePicker => $field->minDate('now'),
);2
3
4
5
6
7
Tanggal disimpan sebagai Carbon instance, bukan string. Submitted value tetap berupa string. Jika column belum memiliki cast, konversikan saat keluar:
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Date;
use PandaPanel\Forms\Components\DatePicker;
DatePicker::make('starts_on')->mutateUsing(
static fn (mixed $value, ?Model $record): mixed => $value === null
? null
: Date::parse((string) $value)->startOfDay(),
);2
3
4
5
6
7
8
9
Hal yang perlu diperhatikan
Tidak ada timezone handling otomatis. Field memformat dan mem-parse berdasarkan timezone yang sedang digunakan Carbon. Panel untuk user lintas timezone harus melakukan konversi melalui formatUsing() dan mutateUsing(); field tidak menebak timezone user.
minDate dan maxDate berupa string, bukan DateTimeInterface. Gunakan '2026-01-01' atau Date::now()->toDateString(), bukan object Carbon langsung.
TimePicker tidak memiliki minTime / maxTime. Jika perlu, batasi melalui rules(); built-in field hanya menghasilkan date_format.
Column tanpa cast melewati formatting. DateTimePicker dan TimePicker meneruskan string apa adanya. Apa yang dikembalikan database itulah yang diterima control. Cast attribute atau bentuk value melalui formatUsing().
Bounds selalu inclusive. Field menghasilkan before_or_equal dan after_or_equal, bukan strict variant. maxDate('today') tetap mengizinkan hari ini.
Lihat juga
- Number — scalar field lain yang memiliki bounds
- Visibility
- State Lifecycle —
formatUsing()danmutateUsing() - Validation
- Table Filters — date range filter pada column yang sama
- Forms and Schemas