Interaktivní komponenty
Komponenty jsou samostatné znovupoužitelné objekty, které vkládáme do stránek. Mohou to být formuláře, datagridy, ankety, vlastně cokoliv, co má smysl používat opakovaně. Ukážeme si:
- jak používat komponenty?
- jak je psát?
- co jsou to signály?
Nette má v sobě vestavěný komponentový systém. Něco podobného mohou pamětníci znát z Delphi nebo ASP.NET Web Forms, na něčem vzdáleně podobném je postaven React nebo Vue.js. Nicméně ve světě PHP frameworků jde o unikátní záležitost.
Přitom komponenty zásadním způsobem ovlivňují přístup k tvorbě aplikací. Můžete totiž stránky skládat z předpřipravených jednotek. Potřebujete v administraci datagrid? Najdete jej na Componette, repositáři open-source doplňků (tedy nejen komponent) pro Nette a jednoduše vložíte do presenteru.
Do presenteru můžete začlenit libovolný počet komponent. A do některých komponent můžete vkládat další komponenty. Vzniká tak komponentový strom, jehož kořenem je presenter.
Tovární metody
Jak se do presenteru komponenty vkládají a následně používají? Obvykle pomocí továrních metod.
Továrna na komponenty představuje elegantní způsob, jak komponenty vytvářet teprve ve chvíli, kdy jsou skutečně
potřeba (lazy / on demand). Celé kouzlo spočívá v implementaci metody s názvem createComponent<Name>()
,
kde <Name>
je název vytvářené komponenty, a která komponentu vytvoří a vrátí.
class DefaultPresenter extends Nette\Application\UI\Presenter
{
protected function createComponentPoll(): PollControl
{
$poll = new PollControl;
$poll->items = $this->item;
return $poll;
}
}
Díky tomu, že jsou všechny komponenty vytvářeny v samostatných metodách, získává kód na přehlednosti.
Názvy komponent začínají vždy malým písmenem, přestože se v názvu metody píší s velkým.
Továrny nikdy nevoláme přímo, zavolají se samy ve chvíli, kdy komponentu poprvé použijeme. Díky tomu je komponenta vytvořena ve správný okamžik a pouze v případě, když je skutečně potřeba. Pokud komponentu nepoužijeme (třeba při AJAXovém požadavku, kdy se přenáší jen část stránky, nebo při cachování šablony), nevytvoří se vůbec a ušetříme výkon serveru.
// přistoupíme ke komponentě a pokud to bylo poprvé,
// zavolá se createComponentPoll() která ji vytvoří
$poll = $this->getComponent('poll');
// alternativní syntax: $poll = $this['poll'];
V šabloně je možné vykreslit komponentu pomocí značky {control}. Není proto potřeba manuálně komponenty předávat do šablony.
<h2>Hlasujte</h2>
{control poll}
Hollywood style
Komponenty běžně používají jednu svěží techniku, které rádi říkáme Hollywood style. Určitě znáte okřídlenou větu, kterou tak často slyší účastníci filmových konkurzů: „Nevolejte nám, my vám zavoláme“. A právě o tu jde.
V Nette totiž místo toho, abyste se museli neustále na něco ptát („byl formulář odeslaný?“, „bylo to validní?“ nebo „stiskl uživatel tohle tlačítko?“), řeknete frameworku „až se to stane, zavolej tuhle metodu“ a necháte další práci na něm. Pokud programujete v JavaScriptu, tento styl programování důvěrně znáte. Píšete funkce které se volají, až nastane určitá událost. A jazyk jim předá příslušné parametry.
Tohle zcela mění pohled na psaní aplikací. Čím víc úkolů můžete nechat na frameworku, tím méně máte práce vy. A tím méně toho můžete třeba opomenout.
Píšeme komponentu
Pod pojmem komponenta obvykle myslíme potomka třídy Nette\Application\UI\Control. (Přesnější
by tedy bylo používat termín „controls“, ale „kontroly“ mají v češtině zcela jiný význam a spíš se ujaly
„komponenty“.) Samotný presenter Nette\Application\UI\Presenter je
mimochodem také potomkem třídy Control
.
use Nette\Application\UI\Control;
class PollControl extends Control
{
}
Vykreslení
Už víme, že k vykreslení komponenty se používá značka {control componentName}
. Ta vlastně zavolá metodu
render()
komponenty, ve které se postáráme o vykreslení. K dispozici máme, úplně stejně jako v presenteru,
Latte šablonu v proměnné $this->template
, do
které předáme parametry. Na rozdíl od presenteru musíme uvést soubor se šablonou a nechat ji vykreslit:
public function render(): void
{
// vložíme do šablony nějaké parametry
$this->template->param = $value;
// a vykreslíme ji
$this->template->render(__DIR__ . '/poll.latte');
}
Značka {control}
umožňuje do metody render()
předat parametry:
{control poll $id, $message}
public function render(int $id, string $message): void
{
// ...
}
Někdy se může komponenta skládat z několika částí, které chceme vykreslovat odděleně. Pro každou z nich si
vytvoříme vlastní vykreslovací metodu, zde v příkladu třeba renderPaginator()
:
public function renderPaginator(): void
{
// ...
}
A v šabloně ji pak vyvoláme pomocí:
{control poll:paginator}
Pro lepší pochopení je dobré vědět, jak se tato značka přeloží do PHP.
{control poll}
{control poll:paginator 123, 'hello'}
se přeloží jako:
$control->getComponent('poll')->render();
$control->getComponent('poll')->renderPaginator(123, 'hello');
Metoda getComponent()
vrací komponentu poll
a nad touto komponentou volá metodu
render()
, resp. renderPaginator()
pokud je jiný způsob renderování uveden ve značce za
dvojtečkou.
Pozor, pokud se kdekoliv v parametrech objeví =>
, všechny parametry budou zabaleny do
pole a předány jako první argument:
{control poll, id: 123, message: 'hello'}
se přeloží jako:
$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']);
Vykreslení sub-komponety:
{control cartControl-someForm}
se přeloží jako:
$control->getComponent("cartControl-someForm")->render();
Komponenty, stejně jako presentery, předávají do šablon několik užitečných proměnných automaticky:
$basePath
je absolutní URL cesta ke kořenovému adresáři (např./eshop
)$baseUrl
je absolutní URL ke kořenovému adresáři (např.http://localhost/eshop
)$user
je objekt reprezentující uživatele$presenter
je aktuální presenter$control
je aktuální komponenta$flashes
pole zpráv zaslaných funkcíflashMessage()
Signál
Už víme, že navigace v Nette aplikaci spočívá v odkazování nebo přesměrování na dvojice
Presenter:action
. Ale co když jen chceme provést akci na aktuální stránce? Například změnit řazení
sloupců v tabulce; smazat položku; přepnout světlý/tmavý režim; odeslat formulář; hlasovat v anketě; atd.
Tomuto druhu požadavků se říká signály. A podobně jako akce vyvolávají metody action<Action>()
nebo render<Action>()
, signály volají metody handle<Signal>()
. Zatímco pojem akce (nebo
view) souvisí čistě jen s presentery, signály se týkají všech komponent. A tedy i presenterů, protože
UI\Presenter
je potomkem UI\Control
.
public function handleClick(int $x, int $y): void
{
// ... processing of signal ...
}
Odkaz, který zavolá signál, vytvoříme obvyklým způsobem, tedy v šabloně atributem n:href
nebo značkou
{link}
, v kódu metodou link()
. Více v kapitole Vytváření odkazů URL.
<a n:href="click! $x, $y">click here</a>
Signál se vždy volá na aktuálním presenteru a action, není možné jej vyvolat na jiném presenteru nebo jiné action.
Signál tedy způsobí znovunačtení stránky úplně stejně jako při původním požadavku, jen navíc zavolá obslužnou metodu signálu s příslušnými parametry. Pokud metoda neexistuje, vyhodí se výjimka Nette\Application\UI\BadSignalException, která se uživateli zobrazí jako chybová stránka 403 Forbidden.
Snippety a AJAX
Signály vám možná trošku připomínají AJAX: handlery, které se vyvolávají na aktuální stránce. A máte pravdu, signály se opravdu často volají pomocí AJAXu a následně přenášíme do prohlížeče pouze změněné části stránky. Neboli tzv. snippety. Více informací naleznete na stránce věnované AJAXu.
Flash zprávy
Komponenta má své vlastní úložiště flash zpráv nezávislé na presenteru. Jde o zprávy, které např. informují o výsledku operace. Důležitým rysem flash zpráv je to, že jsou v šabloně k dispozici i po přesměrování. I po zobrazení zůstanou živé ještě dalších 30 sekund – například pro případ, že by z důvodu chybného přenosu uživatel dal stránku obnovit – zpráva mu tedy hned nezmizí.
Zasílání obstarává metoda flashMessage. Prvním
parametrem je text zprávy nebo objekt stdClass
reprezentující zprávu. Nepovinným druhým parametrem její typ
(error, warning, info apod.). Metoda flashMessage()
vrací instanci flash zprávy jako objekt stdClass
,
které je možné přidávat další informace.
$this->flashMessage('Položka byla smazána.');
$this->redirect(/* ... */); // a přesměrujeme
Šabloně jsou tyto zprávy k dispozici v proměnné $flashes
jako objekty stdClass
, které
obsahují vlastnosti message
(text zprávy), type
(typ zprávy) a mohou obsahovat již zmíněné
uživatelské informace. Vykreslíme je třeba takto:
{foreach $flashes as $flash}
<div class="flash {$flash->type}">{$flash->message}</div>
{/foreach}
Přesměrování po signálu
Po zpracování signálu komponenty často následuje přesměrování. Je to podobná situace jako u formulářů – po jejich odeslání také přesměrováváme, aby při obnovení stránky v prohlížeči nedošlo k opětovnému odeslání dat.
$this->redirect('this') // přesměruje na aktuální presenter a action
Protože komponenta je znovupoužitelný prvek a obvykle by neměla mít přímou vazbu na konkrétní presentery, metody
redirect()
a link()
automaticky interpretují parametr jako signál komponenty:
$this->redirect('click') // přesměruje na signál 'click' téže komponenty
Pokud potřebujete přesměrovat na jiný presenter či akci, můžete to udělat prostřednictvím presenteru:
$this->getPresenter()->redirect('Product:show'); // přesměruje na jiný presenter/action
Persistentní parametry
Persistentní parametry slouží k udržování stavu v komponentách mezi různými požadavky. Jejich hodnota zůstává stejná i po kliknutí na odkaz. Na rozdíl od dat v session se přenášejí v URL. A to zcela automaticky, včetně odkazů vytvořených v jiných komponentách na téže stránce.
Máte např. komponentu pro stránkování obsahu. Takových komponent může být na stránce několik. A přejeme si, aby po
kliknutí na odkaz zůstaly všechny komponenty na své aktuální stránce. Proto z čísla stránky (page
)
uděláme persistentní parametr.
Vytvoření persistentního parametru je v Nette nesmírně jednoduché. Stačí vytvořit veřejnou property a označit ji
atributem: (dříve se používalo /** @persistent */
)
use Nette\Application\Attributes\Persistent; // tento řádek je důležitý
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1; // musí být public
}
U property doporučujeme uvádět i datový typ (např. int
) a můžete uvést i výchozí hodnotu. Hodnoty
parametrů lze validovat.
Při vytváření odkazu lze persistentnímu parametru změnit hodnotu:
<a n:href="this page: $page + 1">next</a>
Nebo jej lze vyresetovat, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu:
<a n:href="this page: null">reset</a>
Persistentní komponenty
Nejen parametry, ale také komponenty mohou být persistentní. U takové komponenty se její persistentní parametry
přenáší i mezi různými akcemi presenteru nebo mezi více presentery. Persistentní komponenty značíme anotací u třídy
presenteru. Třeba takto označíme komponenty calendar
a poll
:
/**
* @persistent(calendar, poll)
*/
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Podkomponenty uvnitř těchto komponent není třeba značit, stanou se persistentní taky.
V PHP 8 můžete pro označení persistentních komponent použít také atributy:
use Nette\Application\Attributes\Persistent;
#[Persistent('calendar', 'poll')]
class DefaultPresenter extends Nette\Application\UI\Presenter
{
}
Komponenty se závislostmi
Jak vytvářet komponenty se závislostmi, aniž bychom si „zaneřádili“ presentery, které je budou používat? Díky chytrým vlastnostem DI kontejneru v Nette lze stejně jako u používání klasických služeb nechat většinu práce na frameworku.
Vezměme si jako příklad komponentu, která má závislost na službě PollFacade
:
class PollControl extends Control
{
public function __construct(
private int $id, // Id ankety pro kterou vytváříme komponentu
private PollFacade $facade,
) {
}
public function handleVote(int $voteId): void
{
$this->facade->vote($id, $voteId);
// ...
}
}
Pokud bychom psali klasickou službu, nebylo by co řešit. O předání všech závislostí by se neviditelně postaral DI
kontejner. Jenže s komponentami obvykle zacházíme tak, že jejich novou instanci vytváříme přímo v presenteru v továrních metodách createComponent…()
. Ale předávat si všechny závislosti
všech komponent do presenteru, abychom je pak předali komponentám, je těžkopádné. A toho napsaného kódu…
Logickou otázkou je, proč prostě nezaregistrujeme komponentu jako klasickou službu, nepředáme ji do presenteru a poté
v metodě createComponent…()
nevracíme? Takový přístup je ale nevhodný, protože komponentu chceme mít
možnost vytvářet klidně i vícekrát.
Správným řešením je napsat pro komponentu továrnu, tedy třídu, která nám komponentu vytvoří:
class PollControlFactory
{
public function __construct(
private PollFacade $facade,
) {
}
public function create(int $id): PollControl
{
return new PollControl($id, $this->facade);
}
}
Takhle továrnu zaregistrujeme do našeho kontejneru v konfiguraci:
services:
- PollControlFactory
a nakonec ji použijeme v našem presenteru:
class PollPresenter extends Nette\Application\UI\Presenter
{
public function __construct(
private PollControlFactory $pollControlFactory,
) {
}
protected function createComponentPollControl(): PollControl
{
$pollId = 1; // můžeme si předat náš parametr
return $this->pollControlFactory->create($pollId);
}
}
Skvělé je, že Nette DI takovéhle jednoduché továrny umí generovat, takže místo jejího celého kódu stačí napsat jenom její rozhraní:
interface PollControlFactory
{
public function create(int $id): PollControl;
}
A to je vše. Nette vnitřně tento interface naimplementuje a předá do presenteru, kde jej už můžeme používat. Magicky
nám právě do naší komponenty přidá i parametr $id
a instanci třídy PollFacade
.
Komponenty do hloubky
Komponenty v Nette Application představují znovupoužitelné součásti webové aplikace, které vkládáme do stránek a kterým se ostatně věnuje celá tato kapitola. Jaké přesně schopnosti taková komponenta má?
- je vykreslitelná v šabloně
- ví, kterou svou část má vykreslit při AJAXovém požadavku (snippety)
- má schopnost ukládat svůj stav do URL (persistentní parametry)
- má schopnost reagovat na uživatelské akce (signály)
- vytváří hierarchickou strukturu (kde kořenem je presenter)
Každou z těchto funkcí obstarává některá z tříd dědičné linie. Vykreslování (1 + 2) má na starosti Nette\Application\UI\Control, začlenění do životního cyklu (3, 4) třída Nette\Application\UI\Component a vytváření hierachické struktury (5) třídy Container a Component.
Nette\ComponentModel\Component { IComponent }
|
+- Nette\ComponentModel\Container { IContainer }
|
+- Nette\Application\UI\Component { SignalReceiver, StatePersistent }
|
+- Nette\Application\UI\Control { Renderable }
|
+- Nette\Application\UI\Presenter { IPresenter }
Životní cyklus componenty
Životní cyklus componenty
Validace persistentních parametrů
Hodnoty persistentních parametrů přijatých z URL zapisuje do properties metoda
loadState()
. Ta také kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a
stránka se nezobrazí.
Nikdy slepě nevěřte persistentním parametrům, protože mohou být snadno uživatelem přepsány v URL. Takto například
ověříme, zda je číslo stránky $this->page
větší než 0. Vhodnou cestou je přepsat zmíněnou metodu
loadState()
:
class PaginatingControl extends Control
{
#[Persistent]
public int $page = 1;
public function loadState(array $params): void
{
parent::loadState($params); // zde se nastaví $this->page
// následuje vlastní kontrola hodnoty:
if ($this->page < 1) {
$this->error();
}
}
}
Opačný proces, tedy sesbírání hodnot z persistentních properties, má na starosti metoda saveState()
.
Signály do hloubky
Signál způsobí znovunačtení stránky úplně stejně jako při původním požadavku (kromě případu, kdy je volán
AJAXem) a vyvolá metodu signalReceived($signal)
, jejíž výchozí implementace ve třídě
Nette\Application\UI\Component
se pokusí zavolat metodu složenou ze slov handle{signal}
. Další
zpracování je na daném objektu. Objekty, které dědí od Component
(tzn. Control
a
Presenter
) reagují tak, že se snaží zavolat metodu handle{signal}
s příslušnými parametry.
Jinými slovy: vezme se definice funkce handle{signal}
a všechny parametry, které přišly s požadavkem, a
k argumentům se podle jména dosadí parametry z URL a pokusí se danou metodu zavolat. Např. jako prametr $id
se
předá hodnota z parametru id
v URL, jako $something
se předá something
z URL, atd.
A pokud metoda neexistuje, metoda signalReceived
vyvolá výjimku.
Signál může přijímat jakákoliv komponenta, presenter nebo objekt, který implementuje rozhraní
SignalReceiver
a je připojený do stromu komponent.
Mezi hlavní příjemce signálů budou patřit Presentery
a vizuální komponenty dědící od
Control
. Signál má sloužit jako znamení pro objekt, že má něco udělat – anketa si má započítat hlas od
uživatele, blok s novinkami se má rozbalit a zobrazit dvakrát tolik novinek, formulář byl odeslán a má zpracovat data a
podobně.
URL pro signál vytváříme pomocí metody Component::link(). Jako
parametr $destination
předáme řetězec {signal}!
a jako $args
pole argumentů, které
chceme signálu předat. Signál se vždy volá na aktuálním presenteru a action s aktuálními parametry, parametry signálu
se jen přidají. Navíc se přidává hned na začátku parametr ?do
, který určuje signál.
Jeho formát je buď {signal}
, nebo {signalReceiver}-{signal}
. {signalReceiver}
je
název komponenty v presenteru. Proto nemůže být v názvu komponenty pomlčka – používá se k oddělení názvu
komponenty a signálu, je ovšem možné takto zanořit několik komponent.
Metoda isSignalReceiver()
ověří, zda je komponenta (první argument) příjemcem signálu (druhý argument). Druhý argument můžeme vynechat – pak
zjišťuje, jestli je komponenta příjemcem jakéhokoliv signálu. Jako druhý parameter lze uvést true
a tím
ověřit, jestli je příjemcem nejen uvedená komponenta, ale také kterýkoliv její potomek.
V kterékoliv fázi předcházející handle{signal}
můžeme vykonat signál manuálně zavoláním metody processSignal(),
která si bere na starosti vyřízení signálu – vezme komponentu, která se určila jako příjemce signálu (pokud není
určen příjemce signálu, je to presenter samotný) a pošle jí signál.
Příklad:
if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) {
$this->processSignal();
}
Tím je signál provedený předčasně a už se nebude znovu volat.