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();
}
URL se skládá z base route + url itemu. Pro příklad výše: /get-started/directory-structure, /get-started/cli-commands atd.

SubmenuItem — parametry

labelstring — Název záložky zobrazený v menu
urlstring|array|null — Relativní URL (připojí se k base route). Pole pro více URL variant.
icon?string — Ikona v menu (HTML)
badgestring|int|null — Badge vedle názvu (např. počet položek)
renderClosure|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
cachebool — 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,
));
cache: false smaže cache pro danou URL před každým načtením. Záložka se vždy načte AJAXem — ideální pro data která se mění v čase.

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();
}
Pravidlo: anonymní funkce pro 5–6 řádků, metoda kontroleru pro cokoliv složitějšího nebo pokud vrací View.

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í
});
HTML záložek se cachuje na frontendu. Pro dynamická data použij cache: false na SubmenuItem — viz sekce Cache výše.