Komponenty

Přesuneme seznam úkolů do komponenty. Budeme jej tak moci snadno použít kdekoliv v aplikaci a také jej budeme moci snadno upravovat.

Často se dostaneme do situace, kdy bychom chtěli stejný nebo podobný prvek aplikace využít na více místech. Nette toto řeší pomocí komponent. Jedná se o třídy, které reprezentují vykreslitelný objekt a je možné je do stránky opakovaně vkládat.

Pojďme tedy naší tabulku s výpisem oddělit do samostatné komponenty.

Základ komponenty

Začneme tím, že si vytvoříme složku app/components. Do ní budeme ukládat námi vytvořené komponenty, a to včetně šablon. V ní si vytvoříme TaskList.php. Třída bude dědit od Nette\Application\UI\Control a bude mít pouze konstruktor a metodu render():

namespace Todo;
use Nette;

class TaskListControl extends Nette\Application\UI\Control
{
	/** @var Nette\Database\Table\Selection */
	private $selected;

	public function __construct(Nette\Database\Table\Selection $selected)
	{
		parent::__construct(); // vždy je potřeba volat rodičovský konstruktor
		$this->selected = $selected;
	}

	public function render(): void
	{
		$this->template->setFile(__DIR__ . '/TaskList.latte');
		$this->template->tasks = $this->selected;
		$this->template->render();
	}
}

Všimněte si, že komponenta se vůbec nezajímá o $parent a $name jako to umí formulář. Nejsou totiž nezbytné, protože budeme komponenty v továrnách připojovat pomocí return. Konstruktor má tedy jediný povinný parametr – výraz, podle kterého budeme úkoly vybírat.

Komponenta může být připojena k jakémukoliv objektu, který rozhraní Nette\ComponentModel\IContainer implementuje. Tím je i objekt Control samotný – do komponenty tak můžeme vnořovat libovolné jiné komponenty. V potomcích Nette\ComponentModel\Container, což jsou i UI\Control a formulář, můžeme použít stejné tovární metody createComponent, jaké jsme používali v presenteru.

Metoda render() zajišťuje vykreslení šablony. Podobně jako u presenteru je i v komponentách k dispozici šablona. Před použitím se jí jen musí nastavit soubor, který se bude vykreslovat. To se udělá voláním setFile(). Pak už jen stačí do šablony přiřadit data, v našem případě tedy výraz, který jsme předali v konstruktoru, a šablonu vykreslit metodou render().

Šablona TaskList.latte bude téměř shodná s tím, co jsme používali v šabloně pro TaskPresenter a HomepagePresenter:

<table>
	<thead>
	<tr>
		<th>Čas vytvoření</th>
		<th>Úkol</th>
		<th>Přiřazeno</th>
	</tr>
	</thead>
	<tbody>
	{foreach $tasks as $task}
	<tr n:class="$iterator->isOdd() ? odd : even">
		<td>{$task->created|date:'j. n. Y'}</td>
		<td>{$task->text}</td>
		<td>{$task->user->name}</td>
	</tr>
	{/foreach}
	</tbody>
</table>

Všimněte si jedné malé změny: u řádku tabulky se objevil atribut n:class. Jedná se o speciální latte makro, které umožňuje pohodlně nastavovat HTML elementům třídy. Jako parametry se mu předávají výrazy podobné ternárním operátorům oddělené čárkami. $iterator->isOdd() ? odd : even ověří, zda je splněno $iterator->isOdd(). Pokud ano, přidá třídu odd, jinak třídu even. Část : even je možno vynechat, pak se v případě nesplněné podmínky nebude přidávat nic.

Proměnná $iterator obsahuje speciální objekt, který Latte vkládá mezi všechny cykly foreach. Pomocí něj můžeme zjišťovat, zda je momentální prvek v pořadí sudý nebo lichý, kolikátý v pořadí je a některé další věci. Více se dozvíte v dokumentaci k makru {foreach}.

Výsledkem je v našem případě „zebrovaná“ tabulka.

Použití

Nyní již zbývá jen komponentu použít. Nejprve v TaskPresenteru upravíme metodu renderDefault() a přidáme createComponentTaskList():

public function renderDefault(int $id): void
{
	$this->template->list = $this->list;
}


protected function createComponentTaskList(): Todo\TaskListControl
{
	if ($this->list === null) {
		$this->error('Wrong action');
	}
	return new Todo\TaskListControl($this->listRepository->tasksOf($this->list));
}

Výběr z databáze se přesunul do metody createComponentTaskList(). Úplně na začátku metody je podmínka a volání metody error() nad presenterem.

if ($this->list === null) {
	$this->error('Wrong action');
}

Metoda error() vyhazuje Nette\Application\BadRequestException s danou zprávou. Je to proto, že komponenty je možné používat napříč různými akcemi. Pokud bychom se snažili ke komponentě přistoupit pomocí úpravy URL, přes jinou akci, ve které by nebylo nastavováno $this->list, skončila by aplikace chybou. Tohle ji před chybou ochrání, protože se na produkci zobrazí prosté „stránka nenalezena“ a ve vývojovém režimu laděnka.

V šabloně Task/default.latte nyní místo tabulky stačí použít makro {control}:

{block content}

<h1>{$list->title}</h1>

<fieldset>
	<legend>Přidat úkol</legend>

	{form taskForm}
	<div class="task-form">
		{label text /} {input text size: 30, autofocus: true} {label userId /} {input userId} {input create}
	</div>
	{/form}
</fieldset>

{control taskList}

{/block}

A to je vše. Nyní podobou úpravu provedeme v HomepagePresenteru:

protected function createComponentIncompleteTasks(): Todo\TaskListControl
{
	return new Todo\TaskListControl($this->taskRepository->findIncomplete());
}

Metoda renderDefault je nyní prázdná, o proto jí můžeme bez obav odstranit. Použití v šabloně Homepage/default.latte je opět velmi jednoduché:

{block content}

<h1>Nesplněné úkoly</h1>

{control incompleteTasks}

{/block}

A to je vše. Úspěšně jsme oba výpisy úkolů nahradili komponentou. Zatím však máme pouze výpis. Bylo by dobré mít možnost i přidané úkoly označit jako splněné…

Signály

Signály umožňují komponentám reagovat na akce od uživatele. Příkladem signálu může být změna řazení tabulky, požadavek na zobrazení podrobnějších informací, odeslání formuláře, nebo právě označení úkolu jako splněného. Signál samotný je předán v adrese a jeho parametry jsou připojeny k aktuálním parametrům stránky. Je tedy realizován novým požadavkem na server.

Před tím, než začneme signál psát, musíme naší komponentě navíc předat objekt modelu, aby vůbec mohla aktualizovat stav úkolu. To vyřešíme přidáním privátního atributu $taskRepository a upravením konstruktoru.

class TaskListControl extends Nette\Application\UI\Control
{
	/** @var Nette\Database\Table\Selection */
	private $selected;

	/** @var TaskRepository */
	private $taskRepository;

	public function __construct(Nette\Database\Table\Selection $selected, TaskRepository $taskRepository)
	{
		parent::__construct(); // vždy je potřeba volat rodičovský konstruktor
		$this->selected = $selected;
		$this->taskRepository = $taskRepository;
	}

Předání modelu také musíme doplnit do metod, které vytvářejí komponentu. V TaskPresenter:

protected function createComponentTaskList(): Todo\TaskListControl
{
	return new Todo\TaskListControl($this->listRepository->tasksOf($this->list), $this->taskRepository);
}

V HomepagePresenter:

protected function createComponentIncompleteTasks(): Todo\TaskListControl
{
	return new Todo\TaskListControl($this->taskRepository->findIncomplete(), $this->taskRepository);
}

Komponenta nyní má k dispozici model a může jej využívat. Přidáme do ní tedy signál markDone. To provedeme vytvořením metody handleMarkDone(), která jako jediný parametr bude mít ID úkolu, který chceme označit jako splněný:

// class TaskListControl

public function handleMarkDone(int $taskId): void
{
	$this->taskRepository->markDone($taskId);
	$this->presenter->redirect('this');
}

Opět si implementujeme metodu v třídě Todo\TaskRepository.

// class TaskRepository

public function markDone(int $id): void
{
	$this->findBy(['id' => $id])->update(['done' => 1]);
}

Metoda update funguje obdobně, jako metoda insert. Před jejím voláním je však nutno specifikovat pomocí where(), jaké záznamy se mají upravit. Je nutné, aby where() bylo před voláním update, jinak se nejprve provede UPDATE bez WHERE (a tím pádem na celé tabulce) a až pak se přidají podmínky, které samozřejmě již nic neovlivní.

Po provedení aktualizace opět musíme provést přesměrování, jinak by se stránka uložila do historie prohlížeče. Metodu redirect() je nutno volat nad presenterem.

Signál z šablony zavoláme následovně:

<table class="tasks">
	<thead>
	<tr>
		<th class="created">&nbsp;</th>
		<th class="list" n:if="$displayList">Seznam</th>
		<th class="text">Úkol</th>
		<th class="user" n:if="$displayUser">Přiřazeno</th>
		<th class="action">&nbsp;</th>
	</tr>
	</thead>
	<tbody>
	{foreach $tasks as $task}
	<tr n:class="$iterator->isOdd() ? odd : even, $task->done ? done">
		<td class="created">{$task->created|date:'j. n. Y'}</td>
		<td class="list" n:if="$displayList">{$task->list->title}</td>
		<td class="text">{$task->text}</td>
		<td class="user" n:if="$displayUser">{$task->user->name}</td>
		<td class="action"><a n:if="!$task->done" n:href="markDone! $task->id" class="icon tick">hotovo</a></td>
	</tr>
	{/foreach}
	</tbody>
</table>

Upravili jsme sloupečky tabulky. V posledním sloupečku je nyní odkaz na označení úkolu jako splněného. n:if zajistí, že se zobrazí pouze u nesplněných úkolů. Odkaz n:href je velmi podobný způsobu odkazování, které jsme již dělali. Jako cíl odkazu je však uveden markDone!, tedy název signálu s vykřičníkem na konci. Signál je možné posílat vždy jen na aktuální akci presenteru, nelze tedy současně s ním zaslat změnu akce. Jinak platí stejná pravidla, jako v případě normálního odkazování – parametry můžeme, ale nemusíme pojmenovávat, pokud je uvedeme ve správném pořadí.

Presenter je také svým způsobem komponentou. To mimo jiné znamená, že i v presenteru můžeme používat signály. Právě proto je na konci odkazu !. Slouží k rozlišení akce od signálu. V případě, že vytváříme odkaz v šabloně komponenty, můžeme vykřičník i vynechat, protože komponenta nemá akce.

Další změnou je přidání třídy done řádkám s již splněnými úkoly. Vidíte tak makro n:class s více třídami v praxi. Také jsme přidali podmínku pro vykreslení uživatele. Oboje později využijeme pro zobrazení na úvodní stránce, kde budeme chtít některé sloupečky skrýt, nebo naopak nechat zobrazit. Pro toto podmíněné vykreslování budeme muset do komponenty ještě dopsat dva atributy:

/** @var boolean */
public $displayUser = true;

/** @var boolean */
public $displayList = false;

public function render(): void
{
	$this->template->setFile(__DIR__ . '/TaskList.latte');
	$this->template->tasks = $this->selected;
	$this->template->displayUser = $this->displayUser;
	$this->template->displayList = $this->displayList;
	$this->template->render();
}

Nyní bychom měli mít následujicí výsledek:

Image

Vytvořili jsme komponentu, která nám umožní pohodlně zobrazit seznam úkolů kdekoliv v aplikaci. Příště se podíváme na přihlašování a ověřování uživatelů.

Zdrojové kódy aplikace v této fázi naleznete na GitHubu.