Jak vytvořit Navigaci: komponenta, presenter, model?
- flamengo
- Člen | 135
Tak já už si vážně nevím rady, tak zkusím shrnout mé závěry a
dotázat se, jak správně navrhnout navigaci.
Jsou tu různé addony, vím, ale ty mi nevyhovují. Potřebuji následující
funkcionalitu:
- Dle jazykové verze se do pole
$items
načtou položky z databáze (v db uloženy názvy, presentery a akce, příklad „:Admin:Users:list“, název: Uživatelé). - Menu musí být multilevel, tedy více úrovní.
- Translátor netřeba, načítá se dle jazykové verze (parametr $locale).
- Navigace zjistí, která sekce je právě aktuální a v poli nastaví
$items[$key]['current'] => TRUE
. - Navigace nastaví
current => TRUE
i pro všechny nadřezená sekce (v šabloně se hodí, šlo by případně dořešit javascriptem). - Drobečková navigace. Vzhledem k tomu, že drobečková navigace je hodně podobná a využívá stejná data, tak bych to spojil.
- Titulek TITLE a nadpis H1 je shodný jako název v menu (nemusí, ale to teď neřešme), takže i tyto údaje by mohla navigace poskytnout.
- Nepřístupné sekce (dle autentizátoru) by se neměli zobrazit. Takže i řešit oprávnění přístupu.
Představa je taková, abych měl v presenteru veškerá data navigace a to kdy budu potřebovat. Například takto (kód pouze přo představu):
protected function startup(){
parent::startup();
$this->navigation->init();
$this->template->items = $this->navigation->items; // položky pro zobrazení menu v šabloně
$this->template->title = $this->getCurrentTitle(); // titulek právě zobrazené stránky
}
KOMPONENTA
Zkoušel jsem vytvořit navigaci jako komponentu Control a narazil jsem na chybu
Nette\InvalidStateException Component '' is not attached to 'Nette\Application\UI\Presenter'
při použití funkce $this->getPresenter()->link()
. Dle rady
zde na fóru jsme kód dopolnil a pak již fungovalo.
// doplnil jsem do komponenty
protected function attached($presenter){
parent::attached($presenter);
$this->loadSections();
}
// poté tento kód fungoval
$this->getPresenter()->link($item['presenter']);
$current = $this->getPresenter()->linkCurrent;
Ale narazil jsem znova. Při vytvoření komponenty v presenteru v metodě
startup()
a dump($this->navigation->items);
jsem
získal prázdné pole. Takže komponenta bohužel mé představy nesplnila.
MODEL
Asi logičtější návrh, řešit data v modelu, oprávnění atd. Potom
z modelu vše předat komponentě, která by pouze zobrazila připravená
data.
Ale v modelu jsem narazil na problém absence funkcí link()
a
isLinkCurrent()
.
Napadlo mne místo link()
použít LinkGenerator()
https://api.nette.org/…nerator.html.
Generuje ale pouze absolutní odkazy, ale to bych asi přežil. V šabloně
bych pak zobrazil odkaz <a href="{$item['link']}">
.
Jak ale zjistit aktuální sekci (modul, presenter, akce, id, …)?
Napadlo mě parsovat URL adresu a porovnat s výsledkem
LinkGenerator()
. Jiné řešení mě nenapadá.
Takže model by se „nějak“ použít asi dal.
PRESENTER
No když o tom tak přemýšlím, tak vyřešit vše v nějaké vlastní
metodě BasePresenteru
, která by se volala ve
startup()
a veškerý výsledek by nasypala do
$this->navigations->items
, by možná vše vyřešilo.
Základní data bych si natáhnul z modelu, auntentizátor bych měl také po
ruce, měl bych k dispozici všechny potřebné funkce link
a
linkCurrent
…
PROSBA
Jak prosím řešíte své navigace Vy? Neříkejte mi, že statiské zapsání
sekcí do config.neon
a následné použití translatoru Vám
postačí, tomu opravdu něvěřím :)
Editoval flamengo (20. 6. 2016 16:15)
- CZechBoY
- Člen | 3608
V presenteru vytvoř komponentu pomocí
protected function createComponentNavigation()
{
return $this->navigationFactory->create();
}
a nech komponentu vykreslit (třeba tu drobečkovou navigaci).
Pokud potřebuješ po navigaci zjistit název aktuální stránky to je to divný, ale můžeš třeba takhle:
public function beforeRender()
{
$this->template->title = $this['navigation']->getTitle();
}
ad generování odkazů: předej si LinkGenerator
ad jsem na téhle url: můžeš z presenteru použít metodu isLinkCurrent
Editoval CZechBoY (20. 6. 2016 16:35)
- flamengo
- Člen | 135
Co by komponenta to nefunguje, už jsem to psal zde https://forum.nette.org/…-attached-to#…
- flamengo
- Člen | 135
Nevytváří se náhodou ta komponenta až při rendrování? Každopádně navržené řešení nefunguje.
protected function createComponentNavigation() {
$this->navigation = $this->navigationFactory->create($this->locale);
return $this->navigation;
}
public function beforeRender(){
dump($this->navigation->items);
}
Chyba dump($this->navigation->items);
Notice Trying to get property of non-object
Zkusil jsme vytvořit komponentu ve startup()
.
protected function startup(){
parent::startup();
$this->navigation = $this->navigationFactory->create($this->locale);
}
public function beforeRender(){
dump($this->navigation->items);
}
Výsledek:
array ()
Také nefunguje :(
- David Matějka
- Moderator | 6445
Ty komponenty si neukladej nikam do clenskych promennych. udelej to tak, jak pise @CZechBoY
- greeny
- Člen | 405
Píšu z hlavy, takže pravděpodobně bude obsahovat spoustu chyb :) ale sám řeším navigaci nějak takhle:
class NavigationControl extends Control
{
/** @var NavigationModel */
private $navigationModel;
/** @var string */
private $lang;
public function __construct($lang, NavigationModel $navigationModel)
{
$this->lang = $lang;
$this->navigationModel = $navigationModel;
}
public function render()
{
$this->template->setFile(__DIR__ . '/default.latte');
$this->template->items = $this->navigationModel->getMenuItems($this->lang);
$this->template->render();
}
public function renderBreadcrumbs()
{
$this->template->setFile(__DIR__ . '/breadcrumbs.latte');
$this->template->items = $this->navigationModel->getBreadcrumbsItems($this->lang);
$this->template->render();
}
}
<!-- default.latte -->
<div class="menu">
<ul n:foreach="$items as $item">
{if $user->isAllowed($item->getResource(), $item->getPrivilege()}
<li n:class="$presenter->isLinkCurrent($item->getLink(), $item->getLinkArgs()) ? current" n:href="$item->getLink(), $item->getLinkArgs()">
{$item->getTitle()}
{if $item->hasChildren()}
<ul n:foreach="$item->getChildren() as $child">
{if $user->isAllowed($child->getResource(), $child->getPrivilege()}
<li n:class="$presenter->isLinkCurrent($child->getLink(), $child->getLinkArgs()) ? current" n:href="$child->getLink(), $child->getLinkArgs()">
{$child->getTitle()}
</li>
{/if}
</ul>
{/if}
</li>
{/if}
</ul>
</div>
<!-- obdobně breadcrumb.latte (tam vypíšeš jen ty co jsou current + jejich rodiče) -->
Samozřejmě si nahraď NavigationModel za nějakej tvůj vlastní model, itemy může vracet i v podobě pole (já tu použil objektový přístup).
(offtopic: pokud chceš i více zanoření v menu, tak je lepší tu šablonu přepsat pomocí nějakého define, aby se tam neduplikoval kód)
V presenteru pak už jednoduše vyrobíš tuhle komponentu třeba za pomocí generovaných továrniček z interface :)
interface INavigationControlFactory
{
/** @return NavigationControl */
function create($lang);
}
BasePresenter:
class BasePresenter extends Presenter
{
/** @var string @persistent */
public $lang;
/** @var INavigationControlFactory @inject */
public $navigationControlFactory;
protected function createComponentNavigation()
{
return $this->navigationControlFactory->create($this->lang);
}
}
šablona:
{control navigation}
{control navigation:breadcrumbs}
Snad to pomůže :)
EDIT přidána podpora pro lang z presenteru
Editoval greeny (20. 6. 2016 19:43)
- flamengo
- Člen | 135
2 David Matějka, CZechBoY:
Popletl jsem to. Nevšiml jsem si $this['navigation']
a pochopil to
jako $this->navigation
.
Takže pokud v presenteru vytvořím komponentu, tak ji mám dostupnou jako
$this['nazev_komponenty']
? Tento fakt jsme bohužel nevěděl a
rázem je všechno jinak.
Díky moc za trpělivost.
2 greeny:
Díky moc na ukázku, prostuduji.
Editoval flamengo (20. 6. 2016 18:09)
- CZechBoY
- Člen | 3608
@flamengo Ano,
$this['navigation']
je to stejné jako
$this->getComponent('navigation', true);
@greeny Máš chybku u @inject továrny – property musí
být public
.
/** @var INavigationControlFactory @inject */
public $navigationControlFactory;
Editoval CZechBoY (20. 6. 2016 18:26)
- erikbalog
- Člen | 27
@greeny A ako vyzerá ten model? Zaujímalo by ma čo sa nachádza pod getResource() getLinkArgs() resp. ako to je v DB zapísané.. Neviem si dať rady ako na navigáciu z databázy pomocou Doctrine. Návrh tej komponenty sa mi páči ;)
greeny napsal(a):
Píšu z hlavy, takže pravděpodobně bude obsahovat spoustu chyb :) ale sám řeším navigaci nějak takhle:
class NavigationControl extends Control { /** @var NavigationModel */ private $navigationModel; /** @var string */ private $lang; public function __construct($lang, NavigationModel $navigationModel) { $this->lang = $lang; $this->navigationModel = $navigationModel; } public function render() { $this->template->setFile(__DIR__ . '/default.latte'); $this->template->items = $this->navigationModel->getMenuItems($this->lang); $this->template->render(); } public function renderBreadcrumbs() { $this->template->setFile(__DIR__ . '/breadcrumbs.latte'); $this->template->items = $this->navigationModel->getBreadcrumbsItems($this->lang); $this->template->render(); } }
<!-- default.latte --> <div class="menu"> <ul n:foreach="$items as $item"> {if $user->isAllowed($item->getResource(), $item->getPrivilege()} <li n:class="$presenter->isLinkCurrent($item->getLink(), $item->getLinkArgs()) ? current" n:href="$item->getLink(), $item->getLinkArgs()"> {$item->getTitle()} {if $item->hasChildren()} <ul n:foreach="$item->getChildren() as $child"> {if $user->isAllowed($child->getResource(), $child->getPrivilege()} <li n:class="$presenter->isLinkCurrent($child->getLink(), $child->getLinkArgs()) ? current" n:href="$child->getLink(), $child->getLinkArgs()"> {$child->getTitle()} </li> {/if} </ul> {/if} </li> {/if} </ul> </div> <!-- obdobně breadcrumb.latte (tam vypíšeš jen ty co jsou current + jejich rodiče) -->
Samozřejmě si nahraď NavigationModel za nějakej tvůj vlastní model, itemy může vracet i v podobě pole (já tu použil objektový přístup).
(offtopic: pokud chceš i více zanoření v menu, tak je lepší tu šablonu přepsat pomocí nějakého define, aby se tam neduplikoval kód)
V presenteru pak už jednoduše vyrobíš tuhle komponentu třeba za pomocí generovaných továrniček z interface :)
interface INavigationControlFactory { /** @return NavigationControl */ function create($lang); }
BasePresenter:
class BasePresenter extends Presenter { /** @var string @persistent */ public $lang; /** @var INavigationControlFactory @inject */ private $navigationControlFactory; protected function createComponentNavigation() { return $this->navigationControlFactory->create($this->lang); } }
šablona:
{control navigation} {control navigation:breadcrumbs}
Snad to pomůže :)
EDIT přidána podpora pro lang z presenteru
Editoval erikbalog (20. 6. 2016 18:29)
- greeny
- Člen | 405
@CZechBoY díky, opraveno (jak říkám, psal jsem z hlavy)
@erikbalog (a případně @flamengo ): co se týče modelu, to už záleží na daném ORM. Já nejčastěji používám takovouhle tabulku (s ukázkovým řádkem dat):
| id| link| linkArgs| resource| privilege| title| parent_id|
|----------------------------------------------------------------|
| 1| Page:about| NULL| Page| read| O nás| NULL|
Do linkArgs ukládám JSON argumentů, které se přidají do metody link
(např. {"id":"1","userId":3}
– v defaultním routeru by to
vygenerovalo odkaz /page/about/1?userId=3
), zbytek je celkem
samozřejmý. Metody getLink(), getLinkArgs(), atd., použité v šabloně se
dají nahradit za přístup do property ($item->link) i za array access
($item[‚link‘]) – opět záleží na použitém ORM, v Doctrině je
$item entita, v Nette je to ActiveRow.
Metoda getChildren() je trošku tricky v NetteDB, tam se to musí vyřešit joinem na stejnou tabulku, u toho vím, že byly občas problémy, když se to psalo přes ten fluent interface. Dá se to vyřešit nativním query(), resp. queryArgs(). V doctrině je to 1:N vazba, takže tam stačí přidat parent a children properties do dané entity a mělo by to fungovat.
No a metody getMenuItems() a getBreadcrumbsItems() už prostě jen z týhle tabulky vytáhnou potřebný data (případně vyfiltrují, seřadí, …).
- erikbalog
- Člen | 27
Aha tak už chápem tu štruktúru (tiež používam ORM doctrine). Ďakujem :) .. Len by ma ešte zaujímalo kde teda plníš pole linkArgs s json parametrami ako sú id, userId a pod.
greeny napsal(a):
@CZechBoY díky, opraveno (jak říkám, psal jsem z hlavy)
@erikbalog (a případně @flamengo ): co se týče modelu, to už záleží na daném ORM. Já nejčastěji používám takovouhle tabulku (s ukázkovým řádkem dat):
| id| link| linkArgs| resource| privilege| title| parent_id| |----------------------------------------------------------------| | 1| Page:about| NULL| Page| read| O nás| NULL|
Do linkArgs ukládám JSON argumentů, které se přidají do metody link (např.
{"id":"1","userId":3}
– v defaultním routeru by to vygenerovalo odkaz/page/about/1?userId=3
), zbytek je celkem samozřejmý. Metody getLink(), getLinkArgs(), atd., použité v šabloně se dají nahradit za přístup do property ($item->link) i za array access ($item[‚link‘]) – opět záleží na použitém ORM, v Doctrině je $item entita, v Nette je to ActiveRow.Metoda getChildren() je trošku tricky v NetteDB, tam se to musí vyřešit joinem na stejnou tabulku, u toho vím, že byly občas problémy, když se to psalo přes ten fluent interface. Dá se to vyřešit nativním query(), resp. queryArgs(). V doctrině je to 1:N vazba, takže tam stačí přidat parent a children properties do dané entity a mělo by to fungovat.
No a metody getMenuItems() a getBreadcrumbsItems() už prostě jen z týhle tabulky vytáhnou potřebný data (případně vyfiltrují, seřadí, …).
- greeny
- Člen | 405
erikbalog napsal(a):
Aha tak už chápem tu štruktúru (tiež používam ORM doctrine). Ďakujem :) .. Len by ma ešte zaujímalo kde teda plníš pole linkArgs s json parametrami ako sú id, userId a pod.
greeny napsal(a):
…
jak jsem řekl – používám buď JSON, nebo vazební tabulku, pro většinu případů stačí JSON. V mém postu je dán i ukázkový JSON, jen na něj pak musíš zavolat json_encode() (nebo radši Json::encode() z Nette). To uděláš buď v šabloně, nebo v té fetch metodě modelu, nebo nejvíc ideálně přímo v getterech a setterech dané entity:
/**
* @ORM\Entity
*/
class Item
{
/**
* @var string
* @ORM\Column(type="string", nullable=FALSE)
*/
private $linkArgs;
/**
* @return array
*/
public function getLinkArgs()
{
return Json::encode($this->linkArgs);
}
/**
* @param array $linkArgs
* @return $this
*/
public function setLinkArgs(array $linkArgs)
{
$this->linkArgs = Json::decode($linkArgs);
return $this;
}
}