Proof of concept v šabloně nastavitelných šablon, anew latte makro {template}

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

Když bych chtěl mít třeba komponentu, která standardně vykreslí menu jako odrážkovaný seznam, ale já bych ji chtěl na jednom místě použít jinak, tedy až v šabloně nastavit, aby se vykreslila jako číslovaný seznam.

Představoval bych si její použití a upravení například takto:

{control menu,
	'items' => array('http://test.cz' => 'Test'),
	'outerTpl' => '<ol>{:itemsWrapper}</ol>',
}

S tím, že by šablona té komponenty vypadala zjednodušeně následovně:

{default $items = array()}
{default $outerTpl = '<ul>{:itemsWrapper}</ul>'}
{default $pairTpl = '<li>{:itemWrapper}</li>'}
{default $linkTpl = '<a href="{:link}">{:description}</a>'}

{capture $itemsWrapper}
	{foreach $items as $link => $description}
		{capture $itemWrapper}
			{template $linkTpl, 'link' => $link, 'description' => $description}
		{/capture}
		{template $pairTpl, 'itemWrapper' => $itemWrapper}
	{/foreach}
{/cpature}

{template $outerTpl, 'itemsWrapper' => $itemsWrapper}

V principu jde o to, že by existovalo latte makro {template}, které by v prvním parametru přijímalo šablonu, ve které by umělo nahradit {:placeholder} za stejnojmenou proměnnou z dalších parametrů makra template.

Co si o tom myslíte? Napadá vás, jak by to šlo jednodušeji? Samozřejmě je mi jasné, že bych mohl komponentě předat adresu na jinou šablonu, ale proč měnit celou šablonu, když chci změnit jen jednu část?

Editoval 2bfree (12. 7. 2016 13:19)

Etch
Člen | 403
+
+2
-

Proč prostě jednoduše nepoužiješ dvě render metody v komponentě?? Tedy něco zhruba následujícího.

Editoval Etch (11. 12. 2014 22:20)

2bfree
Člen | 248
+
0
-

Etch napsal(a):

Proč prostě jednoduše nepoužiješ dvě render metody v komponentě?? Tedy něco zhruba následujícího.

Diky za tip, avsak tady jde trochu o jiny smysl. Pozadavek je renderovat menu a to komkretni renderovani customizovat. Teeba pridat class k <ul> tagu, nebo popis polozky obalit navic do spanu…

Predstav si situaci, kdy jsi uzivatel aplikace, ve ktere mas moznost menit jen sablonu. Mas k dispozici komponenty, ktere by melo jit vyse popsany zpusobem customizovat.

Porad muzes vytvorit zcela novou sablonu, tu predat komponente a trada, ale je to moc prqce s vytvarenim souboru, uploadovanim pres interface aplikace a to jen proto, ze chces drobne upravit nastaveni.

Dalo by se to take chapat treba tak, ze vyvojar upravuje model a presenter a grafik muze ohybat jen template :)

Doufam, ze ted jsem to popsal srozumitelneji.

enumag
Člen | 2118
+
+2
-

Kdysi jsem někde na fóru psal o podobném konceptu, ukázalo se ale že to je nekompatibilní s ajaxem a snippety. Jak funguje ten tvůj koncept v tomto ohledu?

2bfree
Člen | 248
+
0
-

Ještě jsem to nezkoušel. Teprve se na to chystám, ale dříve než začnu, tak se rád ujistím, že nevynalézám kolo, nebo jestli to nejde snáze.

Každopádně díky za tip, co mám prověřit.

Editoval 2bfree (12. 12. 2014 9:06)

enumag
Člen | 2118
+
+1
-

Ono to velmi pravděpodobně fungovat nebude protože při ajaxu se šablona nespouští celá ale jen některé její části (invalid snippety a komponenty). Tzn. to tvé makro se nezavolá a komponenta se bude vykreslovat se špatnou šablonou.

Zdá se že komponenta musí mít šablonu rozhodnutou ještě před vykreslováním, což v praxi znamená že by se šablona komponenty nejspíše měla nastavovat v render metodě(?).

Etch
Člen | 403
+
0
-

Pohled „jsi uzivatel aplikace, ve ktere mas moznost menit jen sablonu. Mas k dispozici komponenty, ktere by melo jit vyse popsany zpusobem customizovat.“:

Tady si nedovedu představit, že „běžný uživatel aplikace“ (zákazník) dokáže vůbec najít volání dané komponenty, natož aby byl schopen změnit:

{control menu, items => $items}

na třeba:

{control menu,
    'items' => array('http://test.cz' => 'Test'),
    'outerTpl' => '<ol>{:itemsWrapper}</ol>',
	'linkTpl' => '<a class="classname" href="{:link}">{:description}</a>'
}

protože aby věděl, co tam vlastně je možné vůbec nastavit, musel by lokalizovat šablonu dané komponenty a na to aby danou šablonu vůbec pochopil by musel zase mít aspoň nějaké základní znalosti latte. „Většina běžných uživatelů“ aplikace, které znám, nevědí ani co je <ol></ol>, natož aby přelouskali latte šablonu. :)

Pohled „Dalo by se to take chapat treba tak, ze vyvojar upravuje model a presenter a grafik muze ohybat jen template“:

Já rozumím tomu jak to myslíš, jen mi to nepřijde v praxi (myšleno v mém osobním případě) moc použitelné. Když si jen pomyslím, že bych podobným způsobem „omezil“ mojí kodérku a grafičku, tak mám pocit, že bych skončil do minuty zadupaný do země.

Samozřejmě si to dovedu představit na nějaké jednoduché úpravy, ale když se kouknu na ten uvedený příklad „menu“, tak ve chvíli kdy se do toho začne cpát {ifCurrent}, přiřazovat linkům nějaké ID, dividery, vnořené menu a podobné věci, už mi to přijde jako nepříliš ideální řešení.

Možná je můj pohled ovlivněn „vztahem“, který mám s kodérem, kdy já nechci dělat nějaký základní template a kodéra degradovat na ohýbače. Já se prostě o šablony nechci starat vůbec, protože to není moje práce. Kodérka dostane jen seznam přístupných proměnných a šablona je její práce. Ona maximálně vznese požadavek, že u dané komponenty musí jít jednoduše vyměnit šablony a to je zase moje práce tohle zařídit.

Existuje zde samozřejmě možnost, že by si pomocí podobného makra kódér nadefinoval šablonu sám, ale přijde mi to, jako by si tím sám sobě svazoval ruce.

Editoval Etch (12. 12. 2014 11:56)

2bfree
Člen | 248
+
0
-

Etch napsal(a):

Pohled „jsi uzivatel aplikace, ve ktere mas moznost menit jen sablonu. Mas k dispozici komponenty, ktere by melo jit vyse popsany zpusobem customizovat.“:

Tady si nedovedu představit, že „běžný uživatel aplikace“ (zákazník) dokáže vůbec najít volání dané komponenty, natož aby byl schopen změnit:

Neuvažuji uživatele jako BFU, ale uživatele „programátora“, který customizuje frontend konfigurací šablon tak, že na vhodná místa umístí potřebné widgety/komponenty/pluginy nebo jak tomu říkat a tím sestaví výslednou aplikaci.

Takže namísto opakované tvorby SignPresenteru, RegisterUserPresenteru, … na nich registrace komponent a následného testování by se „jen“ založila nová šablona a na ní umístila komponenta ;)

Samozřejmě si to dovedu představit na nějaké jednoduché úpravy, ale když se kouknu na ten uvedený příklad „menu“, tak ve chvíli kdy se do toho začne cpát {ifCurrent}, přiřazovat linkům nějaké ID, dividery, vnořené menu a podobné věci, už mi to přijde jako nepříliš ideální řešení.

Pokusil jsem se pro názornost o jednoduchý příklad, ale i celkem komplikované věci se dají podobným způsobem řešit. Osobně beru inspiraci v redakčním systému MODX, kde je právě tento přístup úspěšně aplikován. Příklad komponenty pro vykreslení menu v MODX: Wayfinder

Každopádně díky za postřehy

2bfree
Člen | 248
+
0
-

enumag napsal(a):

Ono to velmi pravděpodobně fungovat nebude protože při ajaxu se šablona nespouští celá ale jen některé její části (invalid snippety a komponenty). Tzn. to tvé makro se nezavolá a komponenta se bude vykreslovat se špatnou šablonou.

Zdá se že komponenta musí mít šablonu rozhodnutou ještě před vykreslováním, což v praxi znamená že by se šablona komponenty nejspíše měla nastavovat v render metodě(?).

Nezbývá tedy než vyzkoušet. O víkendu tomu zkusím věnovat chvíli. Pak poreferuji.

Teď mě ale napadlo, že možná uvažuješ o krok dále než já, tedy že by byl snippet uvnitř takovéto struktury. tedy něco jako:

{capture $flashesWrapper}
	{snippet flashes}
		<div n:foreach="$flashes as $flash">{$flash->message}</div>
	{/snippet}
{/capture}
{template '{:$flashesWrapper}',  flashesWrapper => $flashesWrapper}

Tedy že by se vykreslilo jen to co je mezi {snippet} a {/snippet}. Tohle je zrovna jednoduchý příklad, který by se dal přepsat tak, aby byl snippet kolem celé komponenty, ale máš pravdu, že by v tom mohl být z dlouhodobého pohledu problém. Zkusím promyslet, kudy z toho ven.

Editoval 2bfree (12. 12. 2014 13:38)

David Matějka
Moderator | 6445
+
+3
-

@2bfree nema cenu to zkouset, nebude to fungovat. Pokud je invalidovana pouze komponenta (a ne nejaky wrapper snippet), tak se vzdy zavola pouze render metoda bez parametru.

2bfree
Člen | 248
+
0
-

matej21 napsal(a):

@2bfree nema cenu to zkouset, nebude to fungovat. Pokud je invalidovana pouze komponenta (a ne nejaky wrapper snippet), tak se vzdy zavola pouze render metoda bez parametru.

To je fakt, že na tenhle bug už jsem jednou narazil, kdy jsem měl komponentu vykreslenou přes {control component:list} a ono mi to v ajaxu zavolalo render() namísto renderList().

David Matějka
Moderator | 6445
+
+3
-

@2bfree neni to bug, je to „known limitation“ :)

2bfree
Člen | 248
+
0
-

matej21 napsal(a):

@2bfree neni to bug, je to „known limitation“ :)

Ok, asi by tedy stálo za to tuto „feature“ zmínit v dokumentaci ke snippetům. Něco ve smyslu „Ano, Latte je super a komponenty skvělé, neb mohou mít více než jednu render metodu, ale tato technologie stojí a padá s ajaxem. Pokud chcete používat ajax a snippety, musíte mít pouze jednu render metodu.“

Na jednom projektu jsme tuto limitaci obešli tím, že render metoda ručně zavolala všechny ostatní render metody, kde by teoreticky mohl být snippet, což kupodivu funguje, ale asi by to stálo za menší časovou investici.

enumag
Člen | 2118
+
0
-

Více render metod s ajaxem funguje pokud se už před voláním šablony ví která render metoda se má použít a ta komponenta si to zařídí sama.

2bfree
Člen | 248
+
0
-

Narazil jsem na následující problémy a jejich řešení:

Pokud chci v šabloně definovat parametr, který chci použít ve snippetu, tak o něm snippet neví:

{default $testTpl = '<em>{:test}</em>'}

{snippet test}
	{!=strtr($testTpl, array('{:test}' => $test))}
{/snippet}

Toto se dá naštěstí obejít díky dostupnosti objektu $template

{if isset($template->testTpl) === FALSE}
	{?$template->testTpl ='<em>{:test}</em>'}
{/if}

{snippet test}
	{var $testTpl = $template->testTpl} {* Tohle je skoro zbytečné, jen kvuli ajaxu to zde zatim musi byt (vyřešeno v https://github.com/nette/application/pull/47)*}
	{!=strtr($testTpl, array('{:test}' => $test))}
{/snippet}

Pro vyšší přehlednost zvažuji na výše popsané vytvořit latte makro {varGlobal}{defaultGlobal}

Když invaliduji snippet (zavoláním $presenter->redrawControl('test')), tak se nezavolá to co není ve snippetu.

Na to je jednoduché řešení popsané v dokumentaci k ajaxu tedy že to co chci při invalidaci načíst musím obalito do latte makra {snippetArea} a invalidovat jej společně. Tedy výše napsané by se zapsalo:

{snippetArea defaults}
{if isset($template->testTpl) === FALSE}
	{?$template->testTpl = '<em>{:test}</em>'}
{/if}
{/snippetArea}

{snippet test}
	{var $testTpl = $template->testTpl}
	{=strtr($testTpl, array('{:test}' => $test))|noescape}
{/snippet}

A invalidovalo by se to

$presenter->redrawControl('defaults');
$presenter->redrawControl('test');

Když si komponentu vykreslím přes jinou než render() metodu (jak ukazuje následující příklad), tak se vykreslí správně zavoláním $control->renderTest(), ale v ajaxovém volání ve snippetu se zavolá špatně $control->render() (jak bylo popsáno dříve)

{snippet test}
	{control test:test}
{/snippet}

Tento problém se řeší na několika místech forum , github a našel jsem i různé workaroundy na tento problém: dotblue

Řešení tohoto problému je podle všeho komplikované. Napadlo mě, že by se implementovala funkcionalita, že by šablona komponenty věděla, jakou render metodou byla volána (třeba manuálním zavoláním $control->template->renderedBy = __FUNCTION__, ) a při sestavování odkazu na akci by se název render metody přibalil k názvu komponenty. Což ale nejde rozumně zařídit bez zásahu do stávajících struktur Nette. Má vůbec smysl jít touto cestou, nebo jsem něco nedomyslel?

Poslední problém jsou samotné parametry komponenty předané z šablony:

{snippet test}
	{control test,
		test => $test
	}
{/snippet}

Toto sice krásně funguje při běžném vykreslení, ale při ajaxovém volání se defacto na zavolá $presenter->createComponent('test')->render() dříve než se zavolá render na presenteru (ostatně se ani zavolat nemusí) a tím pádem nemá vůbec ponětí, že by měl dostat nějaké parametry.

K tomuto mě napadlo, že při neajaxovém volání se parametry uchovají v cache a při ajaxovém se vyvolají.

Editoval 2bfree (17. 12. 2014 15:34)

2bfree
Člen | 248
+
0
-

Co se týče latte makra {template ...}, tak už je dokončeno řešení. nedryse/latte-template-macroset

Filip Procházka
Moderator | 4668
+
+1
-

@2bfree velice nebezpečný koncept, podle mě to je celé špatně. Šablony v šablonách jsou overkill. Rozbil jsi kontextově senzitivní escapování, automatickou ochranu proti XSS v odkazech, taky escapování javascriptu a css v properties elementů a bůhví co ještě.

Raději vymysli jak na to použít bloky. A nebo úplně nejlépe neřešit a používat pohledy v komponentách tak, že je budeš konfigurovat v továrničkách – to se s ajaxem slučuje skvěle.

2bfree
Člen | 248
+
0
-

Máš pravdu, že bych měl do dokumentace napsat, že toto macro zcela logicky vypíná content-aware escaping jelikož je vzhledem k potřebě HTML šablon nežádoucí.

Zvažoval jsem i variantu, že bych mohl doplnit escapování všech proměnných vyjma těch, jejichž jméno končí na „Tpl“ a tím zkloubit potřeby a bezpečnost a jak na to tak koukám, asi to tam doplním.

Filipe, možná mám ten pocit, že čím dál častěji máš potřebu se vyjadřovat tímto způsobem, ale výroky typu „je to celé špatně“ věřím že nejen mne poněkud demotivuji a to, že nechápeš smysl použití to neospravedlňuje.

Doporučuji k prostudování redakční systém MODX který používá koncept plně modifikovatelných a tedy použitelných komponent a možná právě proto vyhrál několikrát cenu za nejlepší CMS roku. Samotný redakční systém má své mouchy, ale tento princip přizpůsobitelnosti komponent je IMHO skvělý.

Další problém, který tento nechanismus řeší je problematika translatoru a určení, podle kterého parametru se má počítat plurál. Jelikož dosavadní přístup vyžaduje předávání parametrů ve shodném pořadí s pořadím parametrů v překládaném řetězci, není možné exaktně říct, který parametr je vhodný kandidát na určení plurálu. S tímto mechanismem to může být vždy prvý.

A mimochodem, s ajaxem to nesouvisí. S tím souvisí jiná část tohoto tématu a tu mám již také vyřešenou v podobě konceptu a v brzké době k tomu napíšu i návod.

Editoval 2bfree (28. 12. 2014 9:00)

Filip Procházka
Moderator | 4668
+
0
-

Asi 10 minut jsem přemýšlel, jestli na tohle vůbec reagovat, ale protože téměř všechno escapování a ochrany tam máš opravdu mrtvé, musel jsem na to upozornit. To že to dělá MODX to neospravedlňuje.

toto macro zcela logicky vypíná content-aware escaping jelikož je vzhledem k potřebě HTML šablon nežádoucí

Nesouhlasím :)

Doporučil bych ti vyzkoušet, jestli by ty „mini“ šablonky nešlo prohánět přes Latte místo toho abys vynalézal vlastní šablonovací systém. Psát HTML do stringů a slepovat to do větších stringů je roky překonaný koncept.

Když bys to udělal „správně“, zapisovat by se to mohlo například takto

{var $userName => $user->getIdentity()->getId()}
{template $tpl}<strong>{$userName}</strong>{/template}

A to už jsme vlastně u toho, že zvnovuynalézáš {block} a {capture}, akorát horší, protože tam nemáš vyřešené escapování. Takže jsme zase u toho, že nechápu smysl.


Hele neber si to nijak osobně, tohle není útok, prostě říkám co si myslím. To máš úplně tu stejnou situaci, jako když by ti maminka 15 let tvrdila, že skvěle zpíváš a pak jsi šel do televize a skončil ve hvězdné pěchotě (teď mě napadlo, že to mohlo vyznít, jako bych tvrdil že neumíš programovat, to bych si nikdy nedovolil napsat a pokud by taková intepretace napadlo i tebe tak jsem to tak rozhodně nemyslel).

Budu rád, když mě vyvedeš z omylu.

2bfree
Člen | 248
+
0
-

Jestli jsem správně pochopil funkcionalitu PhpWriteru, tak jsem funkcionalitu escapování vědomně nerozbil, ale naopak jen nezapnul. Tedy měl bych tam doplnit %escape(…), jen to nelze použít pro HTML šablonu tedy ani pro {_ …} makro ve kterém bych potřeboval zvíraznit kus textu.

Proto navrhuji v těchto dvou makrech escapovat pouze obsah vkládaných proměnných a to jen tehdy, neobsahují-li HTML kód záměrně tedy nejsou-li pojmnenovány $…Tpl.


Zkoušel jsem vymyslet jako to „pohánět přes Latte“ jak píšeš, ale došel jsem k tomu, že se vlastnímu makru nevyhnu. Jak tak na to ale koukám, dostal jsem nápad, jak zkombinovat co píšeš s tím co potřebuji já.

Máš pravdu, že se jedná de-facto o {block …} macro, ale s tím rozdílem, že jeho obsah tedy včetně latte macra pro vložení obsahu proměnné potřebuji určit proměnnou. Chápu, že to je „dávno překonaný koncept“, ale pro moji potřebu to IMHO jinak nejde.

Moji snahou je mít možnost při renderování komponenty v šabloně jednoduše modifikovat část renderovaného výstupu. Tedy něco jako:

{control loginForm
	, userNameTpl = 'Zákazník: {:userName}'
}

Tedy že se všechno vykreslí standardně jako dříve, jen se upraví vykreslení jedné malé části komponenty. Vím že to je možné udělat několika dalšími způsoby, ale pro mne je důležitá ta možnost to udělat přímo v šabloně, jelikož šablona je v redakčním systému to jediné, co může administrátor systému měnit.

Samozřejmě, že by bylo možné to řešit následujícím způsobem

{control loginForm
	, template = 'myOwnTemplateOfLoginForm.latte'
}

Tedy, že bych komponentě předal celou vlastní šablonu, kterou bych si pak modifikoval dle potřeby, ale IMHO je to kvůli malé drobné změně jedné malé části zbytečný overhead. Ačkoliv i tato možnost by u komponenty měla být.


Pak je tady ještě ta část s translatorem a placeholdery.


V pohodě Filipe, zkouším to nebrat osobně. Netvrdím, že máš všem říkat jak jsou skvělí. Jsem rád, žes pochopil, že to může vyznít nepříjemně. Vím že si to tak nemyslel, a budu se snažit. :)

Editoval 2bfree (28. 12. 2014 12:10)

Filip Procházka
Moderator | 4668
+
0
-

Ono je asi potřeba se vrátit na začátek a říct si, co vlastně řešíš a proč to řešíš.

Co se týče komponent, napadá mě – co takhle používat extendování šablon? Ty svoje bloky které jdou změnit stejně označuješ. Co kdyby jsi to zkusit udělat už s tím, co latte v tenhle moment umí – pak se vyhneš všem nevýhodám, které jsem zmínil.

MODX znám jen od vidění, nevím jak funguje uvnitř. Dejme tomu, že si to ukážeme třeba na konceptu z wordpresu. Tam máš v šabloně „bloky“ nebo „sidebary“ kam si můžeš naskládat libovolné komponenty v libovolném množství a libovolném pořadí. Měl bys tedy nějaký konfigurátor, pomocí kterého označíš komponentu, jakože má být v daném „šuplíčku“, ve kterém se na webu vykreslí. Když už máš takový konfigurátor, tak ty komponenty nejspíš skládáš v presenterech do sebe dynamicky. Optimalizovat by se to dalo nejakým generováním kódu do tempu, ale to odbíhám.

To znamená, že přímo ovládáš jak se komponenta do presenteru vloží – tedy si s ní můžeš dělat co chceš!

public function actionDefault()
{
	foreach ($sidebarComponents as $meta) {
		// pro jednoduchost, z DIC si vytáhnu továrničku na komponentu
		$factory = $this->serviceLocator->getByType($meta->componentFactoryClass);
		$control = $factory->create();

		// dejme tomu, že máš třeba už v komponentě nastavenou její výchozí šablonu
		// takže uděláme to, že vezmene šablonu co nastavil uživatel v administraci
		// přidáme na její začátek {extends $originalTemplate}
		// a do $originalTemplate předáme cestu k původní šabloně
		// ten latteTmpStorage by měl vzít obsah latte šablony, uložit ji a vrátit cestu k souboru
		$templateFile = $this->latteTmpStorage->build($meta->templateContent, $control->template);

		// tuhle vygenerovanou šablonu pak můžeš nastavit té komponentě
		// akorát si ji nesmíš přepsat v render() :)
		$control->template->setFile($templateFile);

		// připojit k presenteru
		$this->addComponent($control, $meta->controlName);
	}
}

Tohle je samozřejmě jenom nástřel, ale díky podobnému mechanismu teď můžeš udělat třeba to, že máš nějakou výchozí šablonu

<ul n:block="users-list">
	<li n:block="username">{$userName}</li>
<ul>

a v administraci si pak vytvoříš vlastní šablonu

<li n:block="username"><strong>{$userName}</strong></li>

A mělo by to fungovat – změní se jenom ten malej kousek. Detaily určitě zvládneš doladit :)

akadlec
Člen | 1326
+
0
-

nechci vám do toho chlapci zasahovat, ale to cos popsal v posledním postu Filipe jsem řešil taky a začal tak dělat na widget manageru ;)

2bfree
Člen | 248
+
+1
-

Filipe, díky za nasměrování s tím {extend …} makrem. Ještě se na to musím trochu podívat, ale vypadá to, že tudy opravdu vede cesta.