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á?

  1. je vykreslitelná v šabloně
  2. ví, kterou svou část má vykreslit při AJAXovém požadavku (snippety)
  3. má schopnost ukládat svůj stav do URL (persistentní parametry)
  4. má schopnost reagovat na uživatelské akce (signály)
  5. 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.

verze: 4.0 3.x 2.x