Widget Tabel (Table Widgets)
Widget tabel (table widget) adalah sebuah tabel singkat yang dapat diurutkan dan dicari pada sebuah dasbor atau halaman: lima pendaftaran terakhir, sepuluh tagihan terbuka terbesar, pekerjaan (jobs) yang gagal hari ini. Anda menggunakannya ketika jawabannya berupa sekumpulan data (records) alih-alih sebuah angka, dan pembaca perlu melihatnya tanpa harus meninggalkan halaman tempat mereka berada.
Ia dibangun oleh TableSchema yang sama dengan yang digunakan oleh indeks resource (resource index), dan dijalankan oleh TableQuery yang sama, sehingga sebuah kolom akan dirender secara identik di kedua tempat tersebut. Namun, widget ini tetaplah bukan sebuah indeks resource: tidak ada aksi massal (bulk actions), tidak ada manajer kolom (column manager), dan tidak ada tab filter. Widget adalah sebuah ringkasan yang bisa Anda lihat-lihat, bukan tempat kedua untuk mengelola data.
Contoh minimal yang berfungsi
php artisan make:panel-widget RecentUsers --panel=Admin --type=table2
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Widgets;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\TableSchema;
use PandaPanel\Widgets\TableWidget;
final class RecentUsers extends TableWidget
{
protected static ?string $heading = 'Recent sign-ups';
public function table(TableSchema $table): TableSchema
{
return $table->columns([
TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('email'),
]);
}
/**
* @return Builder<User>
*/
public function query(): Builder
{
return User::query()->select(['id', 'name', 'email']);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Lima baris data, sebuah kotak pencarian, header Name yang dapat diurutkan, dan tautan Sebelumnya/Selanjutnya (Previous/Next) ketika ada lebih dari satu halaman.
Kelas (The class)
PandaPanel\Widgets\TableWidget merupakan turunan (extends) dari PandaPanel\Widgets\Widget.
| Anggota (Member) | Signature | Bawaan (Default) |
|---|---|---|
$columnSpan | `protected static int | string |
$emptyMessage | protected static string | 'Nothing to show yet.' |
$perPage | protected static int | 5 |
type() | public static function type(): WidgetType | WidgetType::Table |
table() | abstract public function table(TableSchema $table): TableSchema | — |
query() | abstract public function query(): Builder | — |
data() | public function data(): array | lihat di bawah |
stateNamespace() | public static function stateNamespace(): string | 'widgets.'.kebab(basename) |
protected static string $emptyMessage = 'No one has signed up yet.';
protected static int $perPage = 10;2
3
4
Nilai $perPage sengaja dibuat kecil (singkat): sebuah tabel dasbor ditujukan untuk dibaca sekilas, dan widget menampilkan jumlah tersebut secara sengaja, bukan tanpa alasan.
table()
abstract public function table(TableSchema $table): TableSchema2
Diberikan sebuah PandaPanel\Tables\TableSchema yang baru, dan wajib mengembalikan (return) objek tersebut. Setiap tipe kolom dapat berfungsi di sini — lihat Kolom (Columns).
use PandaPanel\Tables\Columns\DateTimeColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\SortDirection;
public function table(TableSchema $table): TableSchema
{
return $table
->columns([
TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('email')->searchable(),
DateTimeColumn::make('created_at')->label('Joined')->relative()->sortable(),
])
->defaultSort('created_at', SortDirection::Descending);
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
Menambahkan ->searchable() pada kolom mana pun akan membuat kotak pencarian widget muncul. ->sortable() membuat header kolom tersebut dapat diklik. ->defaultSort() menentukan urutan awal sebelum pembaca menyentuh apa pun di halaman.
query()
/** @return Builder<covariant Model> */
abstract public function query(): Builder2
3
Berupa sebuah query builder alih-alih sebuah collection, sehingga table builder dapat melakukan pencarian, pengurutan, dan membaginya ke dalam halaman (pagination).
public function query(): Builder
{
return User::query()->select(['id', 'name', 'email', 'created_at']);
}2
3
4
5
Persempit kuerinya di sini. Pilih (select) hanya kolom-kolom yang ditampilkan oleh tabel — tabel users yang lebar akan menghasilkan payload data yang besar pula — dan batasi kuerinya hanya pada hal yang relevan dengan widget tersebut. Tabel dasbor yang menampilkan seluruh pesanan yang pernah dibuat adalah tipe dasbor yang tidak akan pernah dibuka dua kali oleh siapa pun.
data()
public function data(): array2
Metode data() membangun skema (schema), memaksakan penggunaan perPageOptions([$perPage]) dan defaultPerPage($perPage) menimpa apa pun yang diatur pada table(), menjalankan sebuah PandaPanel\Tables\TableQuery terhadap query() di bawah namespace widget ini, lalu melakukan serialisasi:
[
'columns' => [/* definisi kolom yang sama seperti yang dikirim oleh resource index */],
'rows' => [/* TableSchema::toRow() per record */],
'emptyMessage' => 'No one has signed up yet.',
'state' => ['search' => null, 'sort' => 'created_at', 'direction' => 'desc', /* ... */],
'pagination' => [
'page' => 1,
'perPage' => 5,
'total' => 9,
'lastPage' => 2,
'from' => 1,
'to' => 5,
],
'namespace' => 'widgets.recent-users',
'searchable' => true,
]2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Karena parameter $perPage dipaksakan pada skema, pemanggilan perPageOptions() atau defaultPerPage() di dalam table() tidak akan memberikan efek apa pun. Sebagai gantinya, ubahlah nilai dari properti $perPage.
stateNamespace()
public static function stateNamespace(): string2
Menentukan di mana status tabel (table state) milik widget ini hidup di dalam query string. Nilainya berupa awalan 'widgets.' ditambah dengan nama dasar kelas dalam format kebab-case, jadi kelas RecentUsers akan menjadi widgets.recent-users, menggunakan titik di sisi server dan format tanda kurung siku di dalam URL:
/admin?widgets[recent-users][page]=2&widgets[recent-users][sort]=name&widgets[recent-users][direction]=asc2
Namespace inilah yang membuat dua widget tabel dapat saling berdampingan di dalam satu dasbor yang sama — jika tidak, keduanya akan saling berebut parameter page — dan ini merupakan pengaturan yang sama persis dengan yang sudah digunakan oleh pengelola relasi (relation manager). Lihat Status tabel yang dipertahankan (Persisted table state).
Apa yang digambar oleh perender (What the renderer draws)
| Fitur | Didukung | Catatan |
|---|---|---|
| Kolom dan render sel (Columns and cell rendering) | ya | Identik dengan indeks resource, melalui komponen sel yang sama. |
| Pencarian global (Global search) | ya | Ditampilkan ketika ada kolom yang searchable(). Di-submit dengan menekan Enter atau saat hilang fokus (blur). |
| Pengurutan (Sorting) | ya | Header yang dapat diklik untuk kolom yang sortable(). Akan mereset kembali ke halaman 1. |
| Paginasi (Pagination) | ya | Sebelumnya/Selanjutnya ditambah hitungan dari-sampai dari total, hanya ditampilkan jika lastPage > 1. |
| Empty state (Kondisi kosong) | ya | $emptyMessage ditampilkan dalam satu baris penuh yang membentang. |
| Aksi record (Record actions) | tidak | recordActions() pada skema tidak akan dirender. |
| Aksi header, toolbar, dan massal | tidak | Tidak dirender. |
| Filter dan tab filter | tidak | Tidak ada antarmuka filter yang digambar. |
| Manajer kolom, pengurutan ulang kolom, pengelompokan, ringkasan | tidak | Tidak dirender. |
Jika sebuah tabel widget mulai menumbuhkan banyak tombol toolbar dan filter, itu tandanya ia sebenarnya ingin menjadi sebuah halaman indeks resource. Langkah yang paling tepat adalah memberikan tautan ke halaman tersebut — menggunakan stat yang memiliki url() atau melalui sebuah aksi header (header action) di halaman.
Contoh lengkap (A full example)
Ini adalah isi dari examples/app/Panels/Admin/Widgets/RecentUsers.php:
<?php
declare(strict_types=1);
namespace App\Panels\Admin\Widgets;
use App\Models\User;
use Illuminate\Database\Eloquent\Builder;
use PandaPanel\Tables\Columns\DateTimeColumn;
use PandaPanel\Tables\Columns\TextColumn;
use PandaPanel\Tables\Enums\SortDirection;
use PandaPanel\Tables\TableSchema;
use PandaPanel\Widgets\TableWidget;
final class RecentUsers extends TableWidget
{
protected static int $sort = 20;
protected static string $emptyMessage = 'No one has signed up yet.';
protected static ?string $heading = 'Recent sign-ups';
protected static ?string $description = 'The newest accounts, searchable and sortable.';
public function table(TableSchema $table): TableSchema
{
return $table
->columns([
TextColumn::make('name')->searchable()->sortable(),
TextColumn::make('email')->searchable(),
DateTimeColumn::make('created_at')->label('Joined')->relative()->sortable(),
])
->defaultSort('created_at', SortDirection::Descending);
}
/**
* @return Builder<User>
*/
public function query(): Builder
{
return User::query()->select(['id', 'name', 'email', 'created_at']);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Membatasi cakupan ke halaman (Scoping to the page)
Pada sebuah halaman resource, sebuah widget tabel dapat mempersempit cakupannya secara otomatis pada data yang sedang ditampilkan oleh halaman tersebut:
use Illuminate\Database\Eloquent\Builder;
public function query(): Builder
{
// Pada halaman ListRecords: kueri yang sebenarnya dijalankan oleh indeks, termasuk batasan tab.
return $this->context()->query() ?? User::query();
}2
3
4
5
6
7
8
public function query(): Builder
{
// Pada halaman ViewRecord atau EditRecord: baris-baris data yang menjadi milik record yang sedang ada di layar.
return $this->context()->record()->orders()->getQuery();
}2
3
4
5
6
Pemanggilan context() akan melempar error (throws) ketika widget dirender tanpa adanya konteks tersebut. Oleh karena itu, widget tabel yang ditulis dengan cara ini wajib diletakkan di dalam headerWidgets() atau footerWidgets() pada sebuah halaman resource dan tidak boleh di dasbor. Lihat Gambaran Umum (Overview).
Hal-hal yang perlu diperhatikan (Gotchas)
- Berkas stub
widget-tableyang ditulis oleh generator mendeklarasikan metoderows(): Collectiondan tidak mengimplementasikanquery(). Karena metodequery()bersifat abstrak, kelas yang di-generate tidak akan bisa dimuat (load) sampai Anda mengganti metoderows()menjadiquery(): Builder. Publikasikan stub Anda sendiri menggunakan perintahphp artisan vendor:publish --tag=panda-panel-stubsjika Anda sering melakukan generate untuk widget jenis ini. - Status pencarian (search), pengurutan (sort), dan paginasi (page) hidup di dalam grup query-string
widgets[{id}]yang sama dengan filter milik widget tersebut. Sebuah widget yang mendeklarasikan keduanya akan melihat grup tersebut berstatus "ada" (present) meskipun hanya status tabel (table state) saja yang ada di URL, hal ini menyebabkan filter-filternya di-resolve menjadinullalih-alih menggunakan nilai bawaannya (default). Selalu baca filter menggunakan nilai bawaan secara eksplisit:$this->filter('window', 30). - Pengaturan
perPageOptions()dandefaultPerPage()yang disetel di dalamtable()akan otomatis ditimpa oleh nilai dari$perPage. TableSchema::toRow()memproses (resolves) aksi-aksi record (record actions), sehingga aksi-aksi tersebut akan turut terbawa di dalam payload, tetapi renderer widget tidak akan menggambarnya di layar. Jangan mengandalkan widget sebagai tempat untuk mengeksekusi aksi.- Dua widget tabel yang nama dasar kelasnya (class basenames) memiliki nilai format kebab-case yang identik, akan berbagi namespace yang sama dan akan saling bertabrakan/berebut state. ID widget bersifat unik pada setiap panel, yang otomatis mencegah masalah ini terjadi di dalam satu panel yang sama.
- Kueri akan dijalankan pada setiap kali proses render terjadi, termasuk pada setiap proses poll. Pastikan kuerinya menggunakan indeks (indexed) dan datanya selalu dibatasi kuantitasnya (bounded).