Šablona a layout – renderování v opačném pořadí
- David Grudl
- Nette Core | 8228
Je to už neuvěřitelných dva a půl roku, co jsem popisoval jeden filtr šablon, který se nakonec do distribuce nedostal. Důvodem je hlavně odklon od tehdy perspektivního XML k HTML5 a ztížené podmínky zpracování HTML v PHP.
Protože zatím nemám odpovídající náhradu, použil jsem něco, co vlastně považuju za naprostou zhovadilost, a tím je populární rozdělení šablony na layout + content :-)
Nedostatky dělení na layout a obsah se projeví už u těch
nejjednodušších webů. Pokud chceme mít v <title>
v layoutu stejný titulek jako v <h1>
obsahové šablony,
musíme titulek nastavit jako parametr šablony v presenteru. To stejné platí
pro meta tagy, skripty a styly, jenž jsou specifické jen pro určitou
stránku. Problém je zkrátka v tom, že layout se s každou stránkou také
mění a přesouvat kvůli tomu části šablony do presenteru je
nevyhovující.
Jak věc řešit? Jeden návrh bych měl. Nejprve je třeba, aby v době
renderování layoutu byl již vyrenderován content. K tomu vede celkem
jednoduchá úprava, kterou jsem se rozhodl v revizi
174 zveřejnit. DEPRECATED
Poté je možné v layoutu použít proměnnou…
// @layout.phtml
<head>
<title>{$title}</title>
</head>
…kterou nastavíme v content-šabloně:
// default.phtml
{? $template->title = 'Editace položky'}
<h1>{$template->title}</h1>
...
CurlyBracketsFilter nabízí i syntax:
// default.phtml
{assign $title 'Editace položky'}
<h1>{$title}</h1>
...
- David Grudl
- Nette Core | 8228
LM napsal(a):
SnippetHelper::$outputAllowed = FALSE
by se měl nastavovat ještě před preRenderem, protože teď nefunguje ajax v presenteru.
Zkus být konkrétnější, co přesně nefunguje?
- LM
- Člen | 206
Pokud mám v šabloně (view presenteru, ne šablona komponenty) snippet,
tak v metodě Presenter::renderTemplate
kde se volá
$template->content->preRender()
dojde k tomu že
SnippetHelper::$outputAllowed
bude TRUE
i při Ajax
režimu (pak to končí na: InvalidStateException: Cannot send header after HTTP
headers have been sent (output started at
/var/www/libs/Nette/Templates/Template.php:151).), podmínka která tu hodnotu
mění je až po preRenderu.
Editoval LM (28. 12. 2008 21:46)
- veena
- Člen | 98
David Grudl napsal(a):
Je to už neuvěřitelných dva a půl roku, co jsem popisoval jeden filtr šablon, který se nakonec do distribuce nedostal. Důvodem je hlavně odklon od tehdy perspektivního XML k HTML5 a ztížené podmínky zpracování HTML v PHP.
Protože zatím nemám odpovídající náhradu, použil jsem něco, co vlastně považuju za naprostou zhovadilost, a tím je populární rozdělení šablony na layout + content :-)
Nedostatky dělení na layout a obsah se projeví už u těch nejjednodušších webů. Pokud chceme mít v
<title>
v layoutu stejný titulek jako v<h1>
obsahové šablony, musíme titulek nastavit jako parametr šablony v presenteru. To stejné platí pro meta tagy, skripty a styly, jenž jsou specifické jen pro určitou stránku. Problém je zkrátka v tom, že layout se s každou stránkou také mění a přesouvat kvůli tomu části šablony do presenteru je nevyhovující.Jak věc řešit? Jeden návrh bych měl. Nejprve je třeba, aby v době renderování layoutu byl již vyrenderován content. K tomu vede celkem jednoduchá úprava, kterou jsem se rozhodl v revizi 174 zveřejnit.
Poté je možné v layoutu použít proměnnou…
// @layout.phtml <head> <title>{$title}</title> </head>
…kterou nastavíme v content-šabloně:
// default.phtml {? $template->title = 'Editace položky'} <h1>{$template->title}</h1> ...
CurlyBracketsFilter nabízí i syntax:
// default.phtml {param $title 'Editace položky'} <h1>{$title}</h1> ...
Další možností je využít v layoutu SimpleXML (což má ovšem řadu úskalí):
// @layout.phtml <head> <title>{$content->toXml()->h1}</title> </head>
Pár nápadů.
Tohle mi přijde dost magické nastavovat proměnou pro soubor, který si tento soubor teprve inkluduje.
Ono by možná místo konceptu „layoutu“ bylo lepší použít dedění šablon. Podobně jako je to u presenterů. Vizuální rozložení stránek tomu dědění také odpovídá.
Pak by to mohlo vypadat třeba takhle:
// base.phtml
{param $title 'Editace položky'}
<head>
<title>{$title}</title>
</head>
{block obsah}
<h1>Nejaky obecny H1</h1>
...
{/block}
…kterou nastavím v content-šabloně:
// default.phtml
{extends 'base.html'} // dědí šablonu z base.html
{block obsah} // zde přepíšeme vnitřek zděděného bloku
<h1>{$title}</h1>
{/block}
...
Ohledně toho zpracování (X)HTML v PHP. Supr by bylo mít něco jako
jQuery selector. Voila http://code.google.com/p/phpquery/
Ale používá to PHP5 DOM extension a Zend, takže to možná neni
úplně ono.
S tím PHP parserem, pokud ten DOM v PHPku má stále ty uvedené chyby, co
si nalezl, tak pak už zbývá jen jediná možnost napsat vlastní parser %-(
Našel sem sice ještě tohle: http://sourceforge.net/…mplehtmldom/
Ale nevim, jak to zvládá všechny možný možnosti.
- Ondrej
- Člen | 110
veena napsal(a):
Ono by možná místo konceptu „layoutu“ bylo lepší použít dedění šablon.
Dedení by bylo super :) Takový přistup jsem použival při šablonovaní přes XSLT. Při přechodu na Nette mi toto chybí :(
Např. v hlavním layoutu mam lévý sloupec, který na Homepage obsahuje něco a podstrance se zobrazí jiný(nebo rozsireny) obsah sloupce podle kontextu podstranky. Takze jen v sablone pro podstranku bych zdědil šablonu Homepage a přepsal block levy sloupec jak popisuje veena.
Jak toto resit nyni v Nette? Napada me jen udelat komponentu(control) levy sloupec, kde by se podle presenteru pridaval obsah i za vyuziti dedeni presenteru.
- veena
- Člen | 98
Ondrej napsal(a):
Dedení by bylo super :) Takový přistup jsem použival při šablonovaní přes XSLT. Při přechodu na Nette mi toto chybí :(
Teď jak jsem se kouknul do toho starého prvního článku, tak to tak David měl původně v plánu. Rozhodil mu ale sandál právě ten nespolehlivý HTML parser, jestli to chápu dobře.
- Ondrej
- Člen | 110
veena napsal(a):
Teď jak jsem se kouknul do toho starého prvního článku, tak to tak David měl původně v plánu. Rozhodil mu ale sandál právě ten nespolehlivý HTML parser, jestli to chápu dobře.
jojo, v puvodnim planu se ale pocitalo ze se budou pri dedeni prepisovat primo HTML elementy pojmenovane v id atributu. Tady by pri prepisovani celeho bloku nemusel byt zadny problem pri parsovani za pouziti nette syntaxe {block name}.
- David Grudl
- Nette Core | 8228
LM napsal(a):
Pokud mám v šabloně (view presenteru, ne šablona komponenty) snippet, tak v metodě
Presenter::renderTemplate
kde se volá$template->content->preRender()
dojde k tomu žeSnippetHelper::$outputAllowed
budeTRUE
i při Ajax režimu (pak to končí na: InvalidStateException: Cannot send header after HTTP headers have been sent (output started at /var/www/libs/Nette/Templates/Template.php:151).), podmínka která tu hodnotu mění je až po preRenderu.
Zkusil jsem to přesunout.
- PetrP
- Člen | 587
David Grudl napsal(a):
Říkám si, že celý tento koncept zruším.
Byl to experiment.
O co přesně se zruší?
To že se content vyrenderuje před layoutem, tedy nebude možné nic z tohoto?
Nebo CurlyBrackets syntaxy?
{param $title 'Editace položky'}
Nebo i toto?
{$content->toXml()->h1}
Sám jsem se zatím nedostal k tomu to použít (tedy neznám ty případné problémy), ale chystal jsem se k tomu.
- LM
- Člen | 206
Co třeba takhle: parent template a child template, vlastně je to skoro jak psal veena.
- Ondrej
- Člen | 110
David Grudl napsal(a):
Nejprve je třeba, aby v době renderování layoutu byl již vyrenderován content. K tomu vede celkem jednoduchá úprava,
kterou jsem se rozhodl v revizi 174 zveřejnit.DEPRECATED
Od rev. 187 mi prestalo fungovat logovani SQL dotazu do Firebugu a hlasi mi to chybu, ze hlavicky jiz byly odeslany. Je to tim, ze natazeni polozek z databaze do DataGridu resim az ve fazi renderovani. Konkretne mam v sablone $dataGrid->renderGrid(), ale to uz je pul stranky odeslano do prohlizece a nelze poslat hlavicky pro Firebug. (v rev. 186 to fungovalo, protoze to bylo pred renderovano.)
Zrusena funkce $template->content->preRender(), ktera renderovala content jeste pres samotnym celym renderem, byla dulezita. Je nejaka alternativa? Napadla me jednoducha uprava v presenter::renderTemplate();
<?php
ob_start(); // vystup pozdrzim v bufferu
$template->render(); // v ramci render() se mohou posilat dodatecne http hlavicky
ob_end_flush(); // odeslani bufferu
?>
- David Grudl
- Nette Core | 8228
Pokusím se k tomu napsat víc. Nejprve shrnutí:
Problém
- současná realizace Two-Step view pomocí dělení šablon na layout & content je napytel
- XML je passé, v HTML je problematické detekovat konec elementu, původní řešení https://phpfashion.com/…icke-stranky nelze použít
- je třeba zajistit zpětnou kompatibilitu
Průzkum terénu
- Django template language (zajímavé, používají Template inheritance)
- Rails Layouts (podporuje něco jako
{param $title 'Editace položky'}
, jinak nezajímavé) - Solar Views and Layouts (nezajímavé)
- Zend_Layout (nezajímavé)
- ASP.NET Master Pages (nezajímavé)
- Cake Views (nezajímavé)
- Symfony Views (používá „sloty“, ale je to takové divné)
V podstatě všechny systémy kromě Djanga mají velmi podobné pojetí a liší se v provedení, od zmateného po relativně čisté. Django je velmi milým překvapením (díky Veenovi za odkaz), objektové pojetí šablon je fajn, bohužel se tu nejvíc projevuje rozdím mezi PHP a Pythonem a podobné věci řešit v PHP je dost oříšek (slušně řečeno).
Řešení
V původním návrhu Nette se dědičnost realizovala přes HTML elementy
<div id="sidebar">...</div>
, Django k témuž
používá bloky označené značkami
{% block sidebar %} ... {% endblock %}
. Z pohledu XHTML mi syntax
Djanga připadá jako krok zpět (víc psaní, bloky mohou rozříznout
elementy), z pohledu HTML to vidím jako rozumné řešení. Podporu bloků
jsem tedy zkusmo implementoval (včetně dědičnosti).
Bloky jsou samostatné jednotky, které lze kdekoliv inkludovat, dokonce i uvnitř sebe samého (příklad).
Vzhledem k tomu, že šablony se v takovém případě zpracují ve směru
od vnitřku ven, je možné zachovat podporu pro
{param $title 'Editace položky'}
. Možná by se dalo jít dál a
ke každému souboru template.phtml
přidat volitelný
template.ini
, kde by bylo možné definovat meta informace.
Kompatibilitu se současným stavem bych chtěl zachovat v tom smyslu, že současný stav je vlastně podmnožinou použití bloků & dědičnosti. Zde problém nevidím.
Další háček je v podpoře AJAXů a snippetů. Myslím, že snippety a
bloky by se daly do velké míry sjednotit a díky novým vlastnostem bloků by
mohlo zmizet těžko pochopitelné prefixování zavináčem
@{...}
.
- gawan
- Člen | 110
Neviem či poznáte silverstripe. Chvíľu som s tým robil a tiež sú tam zaujímavo riešené šablóny, stojí za pozretie (Using a subtemplate).
- Ondrej
- Člen | 110
David Grudl napsal(a):
Bloky jsou samostatné jednotky, které lze kdekoliv inkludovat, dokonce i uvnitř sebe samého (příklad).
Uvital bych typ bloku, ktery se hned nevypise, ale zachyti se jen do promenne. V nadrazene layoutove sablone se pak zachyceny blok zobrazi.
Priklad:
@layout.phtml
<?php
<div id="left">
{block #levysloupec}
vychozi obsah, pokud neni levy sloupec v podsablone
{/block}
</div>
<div id="content">
{block #content}
Text na momepage
{/block}
</div>
?>
articles.phtml
<?php
{block #levysloupec}
podobne clanky...
{/block}
{block #content}
seznam clanku...
{/block}
{* ostatni mimo bloky se klasicky pouzije do $content *}
?>
- Petr Daňa
- Člen | 109
Jestli se šablony budou překopávat, tak hlavně prosím zachovejte nějak zpětnou kompatibilitu (nebo možnost na to přepnout) s nynějším stavem, tj. layout + CurlyBracketsFilter. Jinak mě ve firmě zabijou :) (vypadá to, že alespoň částečně na vnitřní věci použijem Nette, teda zatím je to ve fázi zkoušení, tak snad to schválej).
- romansklenar
- Člen | 655
Vlákno ne, jen byla z distribuce odstraněna možnost renderovat v opačném pořadí.
- washo
- Člen | 88
romansklenar napsal(a):
Vlákno ne, jen byla z distribuce odstraněna možnost renderovat v opačném pořadí.
No prave ze mi nebylo jasne co bylo zruseno a co ne…
takze jednoduse:
zruseno: divny zpusob renderovani
ponechano: dedicnost sablon (jak bylo prezentovano v brne na skoleni)
ok?
Editoval washo (5. 2. 2009 20:44)
- David Grudl
- Nette Core | 8228
Jde to, jen to je stále předmětem vývoje:
Presenter:
protected function beforeRender()
{
$this->oldLayoutMode = FALSE;
}
Šablona view:
{extends $layout}
{block #title}Add New Album{/block}
{block #content}
<h1>Add New Album</h1>
{!$form}
{/block}
Šablona layoutu:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{block #title}Nette example{/block}</title>
</head>
<body>
{foreach $flashes as $flash}<div class="flash {$flash->type}">{$flash->message}</div>{/foreach}
<div id="content">
{include #content}
</div>
{if isset($user)}
<p id="logged-in">Logged in as {$user->real_name}. <a href="{link logout}">Logout</a></p>
{/if}
</body>
</html>
- pmg
- Člen | 372
Jen návrh: bylo by pěkné, kdyby šlo uvnitř bloku vložit obsah jeho rodiče.
{* @layout.phtml *}
{block #css}
<link rel="stylesheet" href="{$baseUri}yui/reset-fonts-grids/reset-fonts-grids.css" media="all" type="text/css" />
<link rel="stylesheet" href="{$baseUri}yui/base/base.css" media="all" type="text/css" />
{/block}
{* default.phtml *}
{extends @layout.phtml}
{block #css}
{extends #css} {* vloží obsah bloku z @layout.phtml *}
<link rel="stylesheet" href="{$baseUri}assets/main.css" media="all" type="text/css" />
{/block}
- pmg
- Člen | 372
Aha, špatně jsem to pochopil. Ale jak jsem mohl tušit, že tím
{include #parent}
se myslí
{include #parent}
? :-)
Moje pozorování ohledně zanořených bloků (rev. 223).
{* @layout.phtml *}
{block #a}
aaa
{block #b}
bbb
{/block}
{/block}
{* default.phtml *}
Takto se blok `#b` nevypíše:
{block #a}
AAA
{block #b}
BBB
{/block}
{/block}
Ale lze to obejít takto:
{block #b}
BBB
{/block}
{block #a}
AAA
{include #b}
{/block}
- romansklenar
- Člen | 655
To nikdo pořádně neví :) chtělo by to, aby nám to David osvětlil. Když jsem ale snippety zkoušel použít, dopracoval jsem se akorát k tomuhle.
- pavel80
- Člen | 9
Měl bych návrh:
Tahle diskuze se trochu rozrostla tak snad si toho někdo všimnete:) možná by
se šiklo nové vlákno, příde mi to docela
klíčové téma!
Dědění šablon je super, ale vidím tu jeden problém, kterým možná jde řešit ale nevím jak:
Když např. base.phtml bude vykreslovat menu, kde budu pro to menu zpracovávat data? ve volaném presenteru, který s base.phtml nemá nic společného?
Co kdyby každý presenter měl vlastní @layout!
Zavolám např. MyPresenter, action=myAction; k tomu příslušná šablona je v MyPresenter/myAction, následně by se mohla připojit šablona „nadřazená“ s názvem MyPresenter/@layout.phtml, ta by obsahovala {include content},…
Aby to bylo uplně univerzální mohl by mechanismus kopírovat extends presenterů, což by bylo elegantní že by se dala řešit zmíněná situace s menu a samozdřejmě řada dalších:) Zpětná kompatibilita by byla vyřešení taky neboť by tamnebylo nic jinak, jen něco navíc.
tj. mám např.:
<?php
MyBasePresenter extends BasePresenter implements IComposition {
public function defaultRender() {
// tady bude naplnení hodnot pro vyrenderovani menu
}
}
?>
Tomu náleží šablona MyBase/Default.phtml, IComposition říká že se jedná jen o „obal“
<?php
MyPresenter extends MyBasePresenter implements IComposition {
public function defaultRender() {
parent::defaultRender();
// vyrobit vse co souvisi s hlavni akci a nejake další věci…
}
}
?>
Tomuhle bude odpovídat šablona My/Default.phtml – za {include content}
se vlozi vysledek predchoziho renderovani MyBase
Nakonec se veme @layout.phtml
Ohledání dědičnosti presenterů umí ohlídat reflection, pak už je to jen o načtení šablon…
IComposition jsem zvolil zamerne, neboť MyBasePresenter by nemel být volatelný z url, protože obsahuje jen „obal“ stránky
Co myslíte?
Editoval pavel80 (19. 4. 2009 23:07)
- pavel80
- Člen | 9
Zkusím to ještě z druhé strany
Představte si web strukturou třeba podobný idnes.cz – má několik sekcí, každá vypadá jinak (má jiné šablony), přesto mají něco společného (hlavička, patička). Každá tahle sekce pak obsahuje řadu konkrétních stránek s vlastníma šablonama.
No a teď jak na to? …když nette umí akorát jednu šablonu k akci a jeden společný layout, nebo ne?
- Ondřej Mirtes
- Člen | 1536
pavel80 napsal(a):
Zkusím to ještě z druhé strany
Představte si web strukturou třeba podobný idnes.cz – má několik sekcí, každá vypadá jinak (má jiné šablony), přesto mají něco společného (hlavička, patička). Každá tahle sekce pak obsahuje řadu konkrétních stránek s vlastníma šablonama.
No a teď jak na to? …když nette umí akorát jednu šablonu k akci a jeden společný layout, nebo ne?
Každý Presenter může mít svůj layout a navíc ho můžeš měnit kde
chceš, přes metodu setLayout
.
- Patrik Votoček
- Člen | 2221
pavel80 napsal(a):
Zkusím to ještě z druhé strany
Představte si web strukturou třeba podobný idnes.cz – má několik sekcí, každá vypadá jinak (má jiné šablony), přesto mají něco společného (hlavička, patička). Každá tahle sekce pak obsahuje řadu konkrétních stránek s vlastníma šablonama.
No a teď jak na to? …když nette umí akorát jednu šablonu k akci a jeden společný layout, nebo ne?
Pokud používáš extends a block-y aneb
oldLayoutMode = FALSE . Což je podle davida budoucnost templatů v nette.
Tak ti nic nebrání v tom mít třeba:
@layout.phtml
...hlavicka...
{block #content}
...content...
{/block}
...paticka...
@sekce1.layout.phtml
{extends @layout.phtml}
{block #content}
{block #menu}
...menu...
{/block}
{block #data}
...data...
{/block}
{/block}
@sekce2.layout.phtml
{extends @layout.phtml}
{block #content}
{block #data}
...data...
{/block}
{block #menu}
...menu...
{/block}
{/block}
default.phtml //defaultni akce
{extends @sekce1.layout.phtml} //pokud chces rederovat strukturu sekce2 tak zaměníš sekce1 za sekce2
{block #data}
...data...
{/block}
{block #menu}
...menu...
{/block}
Psal jsem to z hlavy… Takže nezaručuju 100% funkčnost ale jako nástřel by to mělo stačit.
- David Grudl
- Nette Core | 8228
pmg napsal(a):
Moje pozorování ohledně zanořených bloků (rev. 223).
Zanořené bloky by už měly fungovat.
- xificurk
- Člen | 121
Jen pro informaci, kdyby se někdo podivoval proč mu najednou přestali fungovat některé konstrukce:
Nyní je potřeba psát obsah šablony v tomto pořadí:
{assign $var1 'var1'}
{extends 'parent.phtml'}
{block #content}
Tento block nahradí blok #content v šabloně parent.phtml
{/block}
Důležité je uvádět {extends}
až za všemi deklaracemi
{assign}
, které chceme mít dostupné i v rodiči, a až
nazávěr všechny deklarace {block}
, které mají něco
překrýt.
- David Grudl
- Nette Core | 8228
Ano, alespoň v tuto chvíli tomu tak je.
V další revizi bude {extends '@layout.phtml'}
automatické a
nepovinné, tj. pokud bude existovat layout, automaticky se bude extendovat.
Tedy jestli to uživatel nepřebije pomocí vlastní direktivy
{extends neco-jineho}
. Tím vlastně ztrácí na důležitosti
metoda setLayout(), respektive její použití se přesune tam kam patří, do
šablony.
Naopak automatické uzavírání celého obsahu do bloku
{block #content}
bohužel nelze implementovat, mělo by to značná
úskalí. Ale abych život zjednodušil, bude koncová značka nepovinná
(podobně jako je nepovinná značka ?>
v PHP).
Přechod na novou podobu šablon by tak v podstatě znamenal jen
- změnit
{include $content}
na{include #content}
v layoutu - přidat na začátek každé šablony
{block #content}
Uvažuju nad tím, jestli nevypustit tu mřížku, tj.
{block#content}
→ {block content}
. Ale asi ne,
uzavřela by se tím možnost dalšího případného rozšiřování.
Možná bych zrušil (používá to někdo?) zachytávání do proměnné
přes {block $prom}...{/block}
. Dá se to myslím řešit obecněji
přes modifikátor, tj. {block|capture:$prom}...{/block}
. Nebo
přidat srozumitelnější {capture $prom}...{/capture}
.
Bloky jsou totálně dynamické. Lze udělat třeba layout:
<html>
<head>
<title>{block #title|upper}{/block} Nette example</title>
</head>
<body>
{include #content}
</body>
</html>
a obsah ({extends}
je nepovinné)
{block #content}
<h1>{block #title}Edit Album{/block}</h1>
{!$form}
Vygeneruje
<html>
<head>
<title>EDIT ALBUM Nette example</title>
</head>
<body>
<h1>Edit Album</h1>
<form> ... </form>
</body>
</html>