Render Hook
Render hook adalah delapan titik bernama di dalam shell panel tempat panel dapat menyisipkan Vue component miliknya sendiri — misalnya announcement bar di atas setiap page, support link di sidebar, atau status pill di header. Gunakan render hook ketika sesuatu harus tampil pada setiap page panel, atau seluruh page dari satu resource, dan tidak cocok ditempatkan di satu page component tertentu.
Contoh minimal yang berfungsi
Daftarkan hook pada panel:
<?php
declare(strict_types=1);
namespace App\Panels\Admin;
use PandaPanel\Core\Panel;
use PandaPanel\Core\PanelProvider;
use PandaPanel\Enums\RenderHook;
final class AdminPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->path('admin')
->auth()
->renderHook(
RenderHook::PageStart,
'Panels/Admin/Hooks/Announcement',
['message' => 'Maintenance at 5pm'],
);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Buat component pada path yang direpresentasikan nama tersebut:
<!-- resources/js/pages/Panels/Admin/Hooks/Announcement.vue -->
<script setup lang="ts">
defineProps<{ message: string }>();
</script>
<template>
<div class="rounded-md border border-amber-500/40 px-4 py-2 text-sm">
{{ message }}
</div>
</template>2
3
4
5
6
7
8
9
10
Sekarang setiap page Admin panel dimulai dengan bar tersebut. Tidak ada wiring tambahan: panel mengirim hook melalui shared props dan shell merendernya.
Signature
public function renderHook(
RenderHook $hook,
string $component,
array $data = [],
array $scopes = [],
): self2
3
4
5
6
| Parameter | Type | Default | Arti |
|---|---|---|---|
$hook | PandaPanel\Enums\RenderHook | — | Titik shell tempat component dirender |
$component | string | — | Build-time registry key di bawah resources/js/pages/ |
$data | array<string, mixed> | [] | Props yang diteruskan ke component melalui v-bind |
$scopes | list<class-string|string> | [] | Page tempat hook boleh dirender; kosong berarti semua page |
Enum sengaja dibuat closed. Filament dapat menyisipkan Blade pada titik-titik ini, tetapi tidak ada sesuatu yang executable/renderable yang boleh melewati wire di sini. Karena itu hook hanya menyebut component yang sudah diketahui frontend dan membawa props yang dapat diserialisasi. Free string untuk nama hook memungkinkan konfigurasi terhadap titik yang sama sekali tidak dirender shell, yang akan gagal tanpa penjelasan.
Delapan titik hook
| Case | Value | Dirender oleh | Posisi |
|---|---|---|---|
RenderHook::BodyStart | body.start | SidebarPanelLayout.vue, HeaderPanelLayout.vue | Child pertama shell, sebelum sidebar atau top bar |
RenderHook::BodyEnd | body.end | kedua layout yang sama | Child terakhir shell, sebelum toaster |
RenderHook::SidebarStart | sidebar.start | PanelSidebar.vue | Di dalam rail, di atas navigation |
RenderHook::SidebarEnd | sidebar.end | PanelSidebar.vue | Di dalam rail, di bawah navigation |
RenderHook::HeaderStart | header.start | PanelHeader.vue | Setelah sidebar trigger, sebelum breadcrumbs |
RenderHook::HeaderEnd | header.end | PanelHeader.vue | Awal cluster kanan, sebelum search dan bell |
RenderHook::PageStart | page.start | kedua layout | Bagian atas content column, di atas page |
RenderHook::PageEnd | page.end | kedua layout | Bagian bawah content column, setelah page |
body.* dan page.* dirender oleh layout yang dipilih sidebar(variant:), sehingga bekerja pada kedua shell. sidebar.* hanya ada ketika rail dirender, sedangkan header.* hanya ada selama panel mempertahankan topbar — lihat Catatan.
Lokasi component
$component adalah key dalam build-time registry yang di-resolve oleh resources/js/panel/hooks/registry.ts:
const modules = import.meta.glob<{ default: Component }>(
'../../pages/Panels/**/Hooks/*.vue',
);2
3
Key adalah path file relatif terhadap resources/js/pages/, tanpa extension.
| File | Key |
|---|---|
resources/js/pages/Panels/Admin/Hooks/Announcement.vue | Panels/Admin/Hooks/Announcement |
resources/js/pages/Panels/App/Hooks/SupportLink.vue | Panels/App/Hooks/SupportLink |
Glob ini merupakan allowlist secara sengaja: component yang tidak pernah dilihat build tidak dapat dijangkau bagaimanapun nama runtime-nya datang. Dua function tersedia untuk consumer yang perlu memeriksa registry:
import { hasHookComponent, resolveHookComponent } from '@/panel/hooks/registry';
hasHookComponent('Panels/Admin/Hooks/Announcement'); // boolean
resolveHookComponent('Panels/Admin/Hooks/Announcement'); // loader, or null2
3
4
Nama yang tidak dikenal menghasilkan null dan PanelRenderHook.vue tidak merender apa pun. Decorative injection tidak boleh merusak page, sehingga typo hanya menghilangkan dekorasinya. Component dimuat menggunakan defineAsyncComponent, jadi hook yang terdaftar tetapi tidak berada dalam scope suatu page tidak menambah bundle cost untuk page tersebut.
Props
$data diteruskan ke component melalui v-bind. Nilainya harus dapat melewati JSON karena merupakan bagian shared props panel, dibangun sekali per request dan terlihat di page payload.
use PandaPanel\Enums\RenderHook;
$panel->renderHook(RenderHook::SidebarEnd, 'Panels/Admin/Hooks/SupportLink', [
'label' => 'Contact support',
'url' => 'https://support.example.com',
]);2
3
4
5
6
<!-- resources/js/pages/Panels/Admin/Hooks/SupportLink.vue -->
<script setup lang="ts">
defineProps<{ label: string; url: string }>();
</script>
<template>
<a :href="url" class="px-2 py-1 text-sm text-muted-foreground">{{ label }}</a>
</template>2
3
4
5
6
7
8
Value $data bersifat tetap saat panel dikonfigurasi, yaitu ketika provider boot — sebelum request, user, dan URL tersedia. Hook yang membutuhkan informasi tersebut harus membacanya sendiri di component melalui props yang sudah dibagikan panel:
<script setup lang="ts">
import { usePage } from '@inertiajs/vue3';
import { computed } from 'vue';
const page = usePage();
const user = computed(() => page.props.auth.user);
</script>2
3
4
5
6
7
Lebih dari satu hook pada satu titik
Hook bersifat akumulatif. Urutan registrasi adalah urutan render:
$panel
->renderHook(RenderHook::PageStart, 'Panels/Admin/Hooks/First')
->renderHook(RenderHook::PageStart, 'Panels/Admin/Hooks/Second');
array_column($panel->getRenderHooks()['page.start'], 'component');
// ['Panels/Admin/Hooks/First', 'Panels/Admin/Hooks/Second']2
3
4
5
6
Behavior ini memungkinkan plugin menambahkan sesuatu ke titik yang sudah digunakan aplikasi tanpa mengganti hook yang sudah ada.
Membatasi hook ke page tertentu
$scopes membatasi hook ke page tertentu. Kirim class resource atau page; keduanya direduksi menjadi slug saat registration sehingga class name tidak pernah diserialisasi:
use App\Panels\Admin\Pages\Settings;
use App\Panels\Admin\Resources\Users\UserResource;
use PandaPanel\Enums\RenderHook;
use PandaPanel\Pages\Settings\ProfileSettings;
$panel->renderHook(
RenderHook::PageEnd,
'Panels/Admin/Hooks/Note',
scopes: [UserResource::class, Settings::class, ProfileSettings::class],
);
$panel->getRenderHooks()['page.end'][0]['scopes'];
// ['resource:users', 'page:settings', 'page:settings-profile']2
3
4
5
6
7
8
9
10
11
12
13
| Input | Menjadi |
|---|---|
Subclass PandaPanel\Resources\Resource | resource:{slug} |
Subclass PandaPanel\Pages\Page | page:{slug} |
| Value lain | dianggap sudah berupa scope dan dibiarkan apa adanya |
Literal scope juga valid dan menjadi escape hatch ketika class tidak dapat di-import dari provider:
$panel->renderHook(RenderHook::PageEnd, 'Panels/Admin/Hooks/Note', scopes: ['page:custom']);Setiap page melaporkan scope yang dijawabnya melalui metadata page.scope:
| Page | renderHookScope() | Contoh |
|---|---|---|
PandaPanel\Pages\Page | 'page:'.static::slug() | page:settings |
PandaPanel\Resources\Pages\ResourcePage | 'resource:'.static::$resource::slug() | resource:users |
List, create, view, edit, dan relation page milik satu resource semuanya melaporkan scope yang sama. Karena itu scoping ke UserResource::class mencakup seluruh resource, bukan hanya satu screen. Scope list kosong berarti seluruh page pada panel.
Filtering dilakukan di Vue melalui PanelRenderHook.vue, dan pilihan tersebut sebenarnya dipaksakan oleh lifecycle: shared props dibangun di middleware sebelum request mencapai page, sehingga shell mengetahui page yang sedang dirender sedangkan middleware belum.
Membaca kembali hook
/** @return array<string, list<array{component: string, data: array<string, mixed>, scopes: list<string>}>> */
public function getRenderHooks(): array2
Array dikey menggunakan string value hook dan mempertahankan urutan registration:
$panel->getRenderHooks();
// [
// 'page.start' => [
// [
// 'component' => 'Panels/Admin/Hooks/Announcement',
// 'data' => ['message' => 'Maintenance at 5pm'],
// 'scopes' => [],
// ],
// ],
// ]2
3
4
5
6
7
8
9
10
Array yang sama dikirim ke frontend sebagai panel.renderHooks melalui Panel::toSharedArray(). Panel yang tidak mendaftarkan hook mengirim empty map, bukan map berisi delapan empty list.
Dari plugin
Plugin mengonfigurasi panel melalui public API panel, sehingga registration hook dari plugin menggunakan call yang sama:
<?php
declare(strict_types=1);
namespace App\Panels\Plugins;
use PandaPanel\Core\Panel;
use PandaPanel\Enums\RenderHook;
use PandaPanel\Plugins\Plugin;
final class AnnouncementPlugin extends Plugin
{
public function register(Panel $panel): void
{
$panel->renderHook(
RenderHook::BodyStart,
'Panels/Admin/Hooks/Announcement',
['message' => 'Maintenance at 5pm'],
);
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$panel->plugins([new AnnouncementPlugin]);Plugin yang didistribusikan sebagai package harus mem-publish component-nya ke tree resources/js/pages/ milik aplikasi. Registry memakai import.meta.glob pada tree tersebut, sehingga component yang tetap berada di package tidak dapat di-resolve. Lihat Plugin Assets.
Merender hook pada replacement shell
Panel yang mengganti sidebar atau topbar bertanggung jawab menggambar hook point-nya sendiri. Component PanelRenderHook menerima satu prop yang diketik terhadap closed set nama hook:
<!-- resources/js/pages/Panels/Admin/Shell/Sidebar.vue -->
<script setup lang="ts">
import PanelRenderHook from '@/panel/components/PanelRenderHook.vue';
import { useNavigation } from '@/panel/composables/useNavigation';
const { groups } = useNavigation();
</script>
<template>
<aside>
<PanelRenderHook name="sidebar.start" />
<nav v-for="group in groups" :key="group.label ?? ''">
<!-- your navigation -->
</nav>
<PanelRenderHook name="sidebar.end" />
</aside>
</template>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PanelRenderHookName pada resources/js/panel/types/panel.ts mirror enum PHP, sehingga nama yang tidak dikenal shell gagal di type-check daripada hanya merender nothing.
Catatan
- Hook hanya dirender ketika host-nya dirender.
sidebar.startdansidebar.endberada diPanelSidebar.vue, sehingga tidak muncul pada panel yang memakaitopNavigation()/sidebar(variant: 'header'), pada panel yang memanggilnavigation(false), atau replacementsidebarComponent()yang tidak menggambar hook tersebut.header.startdanheader.endberada diPanelHeader.vue, yang dilewati kedua shell ketikatopbar(false). - Nama component yang tidak dikenal gagal secara silent. Tidak ada exception atau log; page tetap dirender tanpa hook. Jika hook tidak muncul, periksa key terhadap path file terlebih dahulu.
- Registry adalah glob terhadap tree aplikasi. File
Hooks/*.vuebaru langsung ditemukan oleh Vite dev server, tetapi production build harus dibangun ulang sebelum key tersedia. - Hook tidak melewati icon registry maupun action registry. Hook hanyalah component biasa dengan props biasa; semua behavior tambahan diimplementasikan component itu sendiri.
- Class-based scope menggunakan default slug resource. Scope direduksi saat provider boot ketika current panel belum tersedia, sedangkan page melaporkan slug yang diberikan panel. Resource yang di-reslug menggunakan
ResourceConfiguration::slug()harus memakai literal scope seperti'resource:people', bukan class resource. - Page tanpa scope hanya cocok dengan unscoped hook.
page.scopedivalidasi saat masuk; jika tidak ada, scoped hook difilter daripada ditampilkan ke semua page. panel:cachetidak meng-cache hook. Manifest hanya meng-cache discovered resource, page, dan widget class. Konfigurasi panel dibangun ulang dari provider pada setiap boot, sehingga perubahan hook tidak membutuhkan cache clear.- Behavior ini dipin oleh
tests/Feature/Panel/RenderHookTest.php.