Jak vytvořit Navigaci: komponenta, presenter, model?

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
flamengo
Člen | 135
+
0
-

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
+
0
-

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
+
0
-

Co by komponenta to nefunguje, už jsem to psal zde https://forum.nette.org/…-attached-to#…

flamengo
Člen | 135
+
0
-

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
+
0
-

Ty komponenty si neukladej nikam do clenskych promennych. udelej to tak, jak pise @CZechBoY

CZechBoY
Člen | 3608
+
0
-

Komponenty jsou vytvářeny až když je potřebuješ – tzn. klidně až při renderování šablony nebo ve startup metodě pokud ji tam využíváš.

greeny
Člen | 405
+
0
-

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
+
0
-

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
+
0
-

@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
+
0
-

@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
+
+1
-

@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
+
0
-

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
+
0
-

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;
	}
}