SubmenuComponent
SubmenuComponent umožňuje vytvořit stránku s více záložkami (submenu). Při prvním načtení se zobrazí layout s menu, následné přepínání záložek probíhá přes AJAX bez reloadu celé stránky.
Základní použití
V kontroleru se použije atribut #[SubmenuBaseRoute] místo běžného #[Route]. Tento atribut matchuje všechny URL s daným prefixem.
use Moony\app\components\controllers\SubmenuComponent;
use Moony\app\components\controllers\submenu\SubmenuItem;
#[SubmenuBaseRoute('get-started')]
public function getStarted(): View
{
$submenu = new SubmenuComponent();
$submenu->addItem(new SubmenuItem(
label: 'Directory Structure',
url: 'directory-structure',
render: new View('account/docs/get-started/directory-structure'),
));
$submenu->addItem(new SubmenuItem(
label: 'CLI Commands',
url: 'cli-commands',
render: new View('account/docs/get-started/cli-commands'),
));
$submenu->addItem(new SubmenuItem(
label: 'Tracy Panel',
url: 'tracy-panel',
render: new View('account/docs/get-started/tracy-panel'),
));
return $submenu->render();
}
SubmenuItem — parametry
| label | string — Název záložky zobrazený v menu |
| url | string|array|null — Relativní URL (připojí se k base route). Pole pro více URL variant. |
| icon | ?string — Ikona v menu (HTML) |
| badge | string|int|null — Badge vedle názvu (např. počet položek) |
| render | Closure|View|null — Obsah záložky. View pro statický obsah, Closure pro dynamický. |
| handlePost | ?Closure — Handler pro POST požadavky na tuto záložku |
| cache | bool — Výchozí true. Při false se obsah záložky nikdy necachuje na frontendu — vždy se načte čerstvý ze serveru. |
Cache
Submenu na frontendu cachuje HTML odpovědi — při přepínání mezi záložkami se obsah načte ze serveru jen jednou a při dalším přepnutí se zobrazí z cache bez AJAX volání. To je výchozí chování (cache: true) a funguje dobře pro statický obsah.
Pokud záložka zobrazuje dynamická data, která se mohou měnit (seznamy, statistiky, stavy), použij cache: false. Při každém přepnutí na tuto záložku se cache smaže a obsah se načte znovu ze serveru.
// Statická záložka (cachuje se — výchozí)
$submenu->addItem(new SubmenuItem(
label: 'About',
url: 'about',
render: new View('account/about'),
));
// Dynamická záložka (vždy čerstvá data)
$submenu->addItem(new SubmenuItem(
label: 'Activity Log',
url: 'activity',
render: static function() {
return new View('account/activity', [
'logs' => ActivityRepository::getRecent(50)
]);
},
cache: false,
));
Render jako Closure
Pokud záložka potřebuje dynamická data, použije se Closure. Parametry closure se automaticky resolvují z Dependency Injection, URL proměnných nebo globalParameters.
$submenu->addItem(new SubmenuItem(
label: 'Detail',
url: 'detail',
render: static function(Request $request) {
$data = ['items' => SomeService::getItems()];
return new View('account/detail', $data);
},
));
POST handling
Pro zpracování formulářů uvnitř záložky slouží handlePost. Volá se automaticky při POST požadavku na danou záložku. Parametry closure se resolvují stejně jako u render.
Pro krátkou logiku (smazání záznamu, jednoduchý update) stačí anonymní funkce. Pro složitější logiku je lepší delegovat na metodu kontroleru:
// Krátká logika — anonymní funkce stačí
$submenu->addItem(new SubmenuItem(
label: 'Tags',
url: 'tags',
render: new View('account/tags'),
handlePost: static function() {
$result = Request::validate([
'tagId' => Validator::number('Neplatné ID')
]);
if($result->success()) {
TagRepository::delete(Request::input('tagId'));
}
Response::reload();
},
));
// Složitější logika — delegace na metodu kontroleru
$submenu->addItem(new SubmenuItem(
label: 'Settings',
url: 'settings',
render: fn() => $this->renderSettings(),
handlePost: fn() => $this->handleSettingsPost(),
));
// Metody ve stejném kontroleru
private function renderSettings(): View
{
return new View('account/settings', [
'settings' => SettingsService::getAll()
]);
}
private function handleSettingsPost(): void
{
$result = Request::validate([
'name' => Validator::notEmpty('Jméno je povinné'),
'email' => [
Validator::email('Neplatný email'),
Validator::mustNotExistsInDb('email', 'users', 'Email již existuje')
]
]);
if($result->success()) {
SettingsService::update(Request::inputAll());
}
Response::reload();
}
Globální parametry
Pokud více záložek sdílí společné proměnné (např. ID entity), lze je nastavit přes addGlobalParameter(). Tyto hodnoty se automaticky předají do Closure parametrů.
$submenu = new SubmenuComponent();
$submenu->addGlobalParameter('userId', $userId);
$submenu->addItem(new SubmenuItem(
label: 'Profile',
url: 'profile',
render: function(int $userId) {
return new View('account/profile', ['user' => UserService::find($userId)]);
},
));
Šablony záložek
Šablony pro submenu záložky jsou běžné Twig soubory bez {% extends %}. SubmenuComponent je automaticky obalí do layoutu s menu.
<!-- app/views/account/docs/get-started/cli-commands.twig -->
<div class="page-content">
<div class="card">
<div class="card-body">
Obsah záložky...
</div>
</div>
</div>
JavaScript — Account.Submenu
Na frontendu zajišťuje přepínání záložek třída Account.Submenu. Inicializuje se automaticky v layoutu pro stránky se submenu.
| Account.Submenu.init() | Registruje popstate handler pro zpět/vpřed v prohlížeči |
| Account.Submenu.setBaseUrl(url) | Nastaví base URL pro relativní cesty záložek |
| Account.Submenu.open(url, options?) | Otevře záložku. options: { pushHistory: false } pro popstate. |
| Account.Submenu.clearCache() | Vyčistí HTML cache všech záložek |
Po přepnutí záložky se dispatchne event account:submenu:page-change na window. Lze na něj navázat vlastní logiku.
window.addEventListener('account:submenu:page-change', (e) => {
// e.detail.from — předchozí URL
// e.detail.to — nová URL
hljs.highlightAll(); // re-highlight kódu po AJAX načtení
});
