Formuláře

Vytvoříme si jednoduchý formulář pro zakládání nových úkolů a další pro vytváření nových seznamů.

Formuláře z Nette lze používat samostatně, nezávisle na zbytku frameworku. Pokud se je ale rozhodneme používat v rámci Nette aplikaci ve spojení s presentery a plným komponentovým modelem, bude jejich používání ještě o něco jednodušší a hlavně radostnější.

Základní třídy sídlí ve jmenném prostoru Nette\Forms. Je zde jak základní třída formuláře, tak veškeré formulářové komponenty a třídy pro vykreslování. Pokud budeme formulář používat v Nette aplikaci, budeme ještě potřebovat třídu Nette\Application\UI\Form, která obsahuje napojení na presenter a využívá signálů a událostí.

Nejlepší ale bude si vše ukázat v praxi.

Formulář pro zadání úkolu

Tento formulář bude zobrazen nad seznamem úkolů v dané kategorii. Jeho definici tedy umístíme do TaskPresenteru. Bude obsahovat políčko s textem a jeden selectbox na výběr uživatele, kterému má být úkol přiřazen.

Abychom mohli vybrat, komu je úkol přiřazen, budeme potřebovat uživatele. Metodu startup() máme v TaskPresenteru už definovanou, takže si ji rozšíříme o získání modelu pro uživatele.

// class TaskPresenter

/** @var Todo\UserRepository */
private $userRepository;

protected function startup(): void
{
	parent::startup();
	$this->listRepository = $this->context->listRepository;
	$this->userRepository = $this->context->userRepository; // získáme model pro práci s uživateli
}

Definice formuláře bude vypadat následovně:

protected function createComponentTaskForm(): Form
{
	$userPairs = $this->userRepository->findAll()->fetchPairs('id', 'name');

	$form = new Form();
	$form->addText('text', 'Úkol:', 40, 100)
		->addRule(Form::FILLED, 'Je nutné zadat text úkolu.');
	$form->addSelect('userId', 'Pro:', $userPairs)
		->setPrompt('- Vyberte -')
		->addRule(Form::FILLED, 'Je nutné vybrat, komu je úkol přiřazen.');
	$form->addSubmit('create', 'Vytvořit');
	return $form;
}

Abychom mohli použít volání new Form(), musíme na začátku souboru uvést deklaraci use Nette\Application\UI\Form;.

Funkce createComponentTaskForm() je speciální tovární funkcí na komponenty. Kdykoliv presenter požádáme o instanci komponenty s názvem taskForm, tak se nejprve podívá, zda již takovou komponentu nemá vytvořenou. Pokud ne, tak si zavolá právě tuto funkci. Funkce buď jen komponentu vytvoří a vrátí ji jako svou návratovou hodnotu, nebo jí rovnou připojí k presenteru.

Tovární metody na komponenty jsou volány samotným presenterem. Neměly by být volány přímo, ale je nutné, aby se k nim dostal presenter, proto musí být protected.

Přímé připojení vypadá takto a je možné jej zavolat kdekoliv v presenteru, nejenom v továrničce

protected function createComponentTaskForm(): Form
{
	$form = new Form($this, 'taskForm');
	//...
	// return $form; by bylo zde zbytečné
}

Všechny třídy, které dědí od Nette\ComponentModel\Component a nebyl jim změněn konstruktor, mohou být takto připojeny k rodiči (v tomhle případě formulář k presenteru). První argument je rodič (presenteru) a druhý název komponenty taskForm. Továrna dostává název komponenty automaticky jako první argument, takže do kódu nemusíme název psát přímo a snížíme tak riziko překlepu:

protected function createComponentTaskForm(string $name): Form
{
	$form = new Form($this, $name);
	// ...
}

Pokud formulář připojíte ihned v konstruktoru, budou si jednotlivé prvky při sestavování průběžně načítat data, která byla odeslána. Což se může hodit, pokud potřebujete podmínit vytvoření některých prvů, nebo validačních pravidel. Jinak jsou oba zápisy funkčně prakticky stejné.

Pojďme si nyní projít jednotlivé prvky formuláře.

$form->addText('text', 'Úkol:', 40, 100)
	->addRule(Form::FILLED, 'Je nutné zadat text úkolu.');

Přidá nové textové políčko s názvem text a popiskou Úkol:. Jeho velikost bude 40 znaků a maximální délka 100. Metoda addRule přidává validační pravidlo. Prvním parametrem je konstanta, která udává typ pravidla. Pravidlo Form::FILLED ověřuje, zda bylo políčko vyplněno. Druhý parametr je nepovinný a definuje hlášku, která se uživateli zobrazí v případě, že pravidlo nebylo splněno. Metoda má ještě třetí nepovinný parametr a tím jsou parametry validace, například v případě pravidla Form::MIN_LENGTH udává tento parametr minimální délku řetězce, který uživatel musí zadat.

Další validační pravidla jsou uvedena v dokumentaci k formulářům. Obecná validační pravidla lze aplikovat na všechny prvky, dále pak má každý prvek vlastní sadu pravidel, která na něj lze aplikovat. Podívejte se například na pravidla k textovému políčku.

$prompt = Html::el('option')->setText("- Vyberte -")->class('prompt');
$form->addSelect('userId', 'Pro:', $userPairs)
	->setPrompt($prompt)	// je možné předat text i prvek HTML
	->addRule(Form::FILLED, 'Je nutné vybrat, komu je úkol přiřazen.');

Tento kód přidává do formuláře selectbox. První dva parametry jsou stejné, jak v předchozím případě. Třetí argument je asociativní pole ve tvaru hodnotapopis volby. Takové pole můžeme získat metodou fetchPairs() zavolanou nad objektem tabulky. fetchPairs('id', 'name') použije jako klíč v poli ID uživatele a jako popisku volby jeho jméno. Metoda setPrompt() přidá na začátek volbu s danou popiskou a prázdnou hodnotou. Taková volba pak nám umožňuje oddělit situaci, kdy uživatel prvek skutečně nevyplnil, a kdy jen ponechal v prvku výchozí hodnotu. V kombinaci s validačním pravidlem Form::FILLED pak způsobí, že uživatel musí vždy nějaký prvek vybrat.

Posledním prvkem je odesílací tlačítko:

$form->addSubmit('create', 'Vytvořit');

Parametry jsou opět stejné. Popiska tlačítka tak bude Vytvořit.

Nyní máme tento jednoduchý formulář téměř kompletní. Zbývá nám jej jen vykreslit. Přesuneme se tedy do šablony Task/default.latte a nad tabulku s výpisem prvků přidáme:

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

	{control taskForm}
</fieldset>

Povšimněte si nového makra {control}. To zajistí vykreslení komponenty se zadaným názvem. V našem případě presenter nejprve zjistí, zda již existuje komponenta s názvem taskForm. Pokud neexistuje, zavolá si metodu createComponentTaskForm a komponentu si vytvoří. Pak zajistí vykreslení pomocí metody render(), kterou má formulář definovanou. Blíže si ji představíme v další části.

Na stránce bychom teď měli vidět následující výsledek:

Image

Zkusíme si cvičně přidat úkol. Vyplníme text úkolu, vybereme komu má být přiřazen a potvrdíme… Ouha, ale nic se nestalo! To proto, že prozatím nemáme napsanou obsluhu odeslaného formuláře. K tomu slouží událost onSuccess, která se vykoná po úspěšném odeslání formuláře, což také znamená, že pokud formulář obsahuje validační pravidla, musí být správně vyplněn. Za přidání tlačítka pro odeslání formuláře tedy přidáme ještě jeden řádek:

$form->addSubmit('create', 'Vytvořit');
$form->onSuccess[] = [$this, 'taskFormSubmitted']; // naše událost pro zpracování formuláře

Ten nastavuje formuláři, že po jeho úspěšném odeslání se má vykonat metoda taskFormSubmitted z objektu $this, tedy z aktuálního presenteru. To ovšem znamená, že jí musíme vytvořit.

public function taskFormSubmitted(Nette\Application\UI\Form $form, stdClass $values): void
{
	$this->taskRepository->createTask($this->list->id, $form->values->text, $form->values->userId);
	$this->flashMessage('Úkol přidán.', 'success');
	$this->redirect('this');
}

Budeme v ní potřebovat zbývající službu taskRepository, takže si ji předáme do presenteru.

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

protected function startup(): void
{
	parent::startup();
	$this->listRepository = $this->context->listRepository;
	$this->userRepository = $this->context->userRepository;
	$this->taskRepository = $this->context->taskRepository; // předání potřebné služby
}

A vytvoříme si v ní metodu, která nám zpracuje vložení záznamu

// class TaskRepository

public function createTask($listId, $task, $assignedUser)
{
	return $this->getTable()->insert([
		'text' => $task,
		'user_id' => $assignedUser,
		'created' => new \DateTime(),
		'list_id' => $listId
	]);
}

Nyní ji můžeme ve zpracování formuláře zavolat.

Narozdíl od tovární metody je zpracování události voláno samotnou komponentou, nikoliv presenterem. Proto musí být public.

Tato metoda dostane jako jediný parametr instanci formuláře, který byl odeslán. Do databáze vloží pomocí metody insert() data nového úkolu. Data jsou předána jako asociativní pole, kde jako klíč je uveden název sloupečku. Povšimněte si, že pro naplnění sloupce created jsme použili instanci DateTime(). Nette samo před vložením čas převede na formát používaný databází. Sloupeček done uvedený není, neboť má výchozí hodnotu 0.

Metoda flashMessage() vypíše uživateli tzv. flash zprávu. Jedná se o jednorázové oznámení o výsledku stavu akce. O jejich vykreslení se pak staráme v šabloně @layout.latte. Druhý parametr udává třídu zprávy, v tomto případě použijeme success. Hlášení pak díky definici stylů z minulé části bude mít krásnou uklidňující zelenou barvu.

Metoda redirect() pak konečně provede přesměrování. Má stejné parametry, jako jsme uváděli v šabloně makru {link}. Klíčové slovo this místo dvojice Presenter:action nás přesměruje na aktuální stránku se stejnými parametry. Toto přesměrování je velmi důležité. Pokud bychom ho neprovedli, uživateli by se uložil odeslaný POST formulář do historie prohlížeče. Pokud by se pak na tuto stránku vrátil, formulář by se odeslal znovu a tím pádem by se záznam v databázi vytvořil ještě jednou. Podobně by se pak chovalo obnovení stránky.

Nyní již vytváření úkolů bude plně funkční. Vzhled formuláře však není ideální. Nette vykresluje formuláře do tabulek, takže se nám do formuláře trochu vmíchal styl seznamu úkolů. Pojďme si to tedy napravit.

Vlastní vykreslení formuláře

Pokud chceme jednotlivé prvky formuláře vykreslit ručně, můžeme tak učinit pomocí latte maker {form}, {input} a párového {label /}. Jejich názvy mluví za vše, proto si je ukážeme v praxi:

{form taskForm}
<div class="task-form">
	{control $form errors}

	{label text /} {input text size: 30, autofocus: true} {label userId /} {input userId} {input create}
</div>
{/form}

Počáteční {form taskForm} říká, že budeme vykreslovat formulář s názvem taskForm. Uvnitř je jeden vnořený <div> a v něm na jedné řádce všechny prvky formuláře.

Ještě před nimi je však nutné zajistit případné vykreslení chyb ve vyplněném formuláři. To zajistíme makrem {control $form errors}. Před odesláním se sice provádí kontrola JavaScriptem u klienta, pokud jej ale bude mít uživatel vypnutý, formulář se odešle a je nutné mu chyby zobrazit takto. Některé chyby navíc není možné u klienta ověřit, proto na výpis chyb nesmíme zapomenout.

Pak hned následují jednotlivé prvky. První {label text /} vykreslí popisku pro prvek text. Jak již bylo řečeno, makro {label /} je párové. Buď můžeme do jeho obsahu napsat popisku přímo v šabloně ({label text}Text:{/label}), nebo makro ukončit podobně jako HTML tag. V takovém případě se použije popiska zadaná při definici formuláře.

Makro {input text} vykreslí samotný formulářový prvek. Volitelně mu lze přidat seznam atributů, které chceme HTML elementu přiřadit. Zde nastavujeme size na 30 a povolujeme autofocus, který je podporován jen v novějších prohlížečích.

Výsledný formulář bude nyní vypadat takto:

Image

Nyní vytvoříme další formulář, tentokrát pro vytvoření seznamu úkolů.

Formulář na vytvoření seznamu úkolů

Tento formulář by bylo vhodné umístit do levého sloupce, ihned pod seznam úkolů. Bude zobrazen na každé stránce, proto jej budeme definovat v BasePresenteru. Protože tento formulář je velmi podobný, rovnou uveďme kód. BasePresenter:

protected function createComponentNewListForm(): Form
{
	$form = new Form();
	$form->addText('title', 'Název:', 15, 50)
		->addRule(Form::FILLED, 'Musíte zadat název seznamu úkolů.');
	$form->addSubmit('create', 'Vytvořit');
	$form->onSuccess[] = [$this, 'newListFormSubmitted'];
	return $form;
}

public function newListFormSubmitted(Nette\Application\UI\Form $form, stdClass $values): void
{
	$list = $this->listRepository->createList($form->values->title);
	$this->flashMessage('Seznam úkolů založen.', 'success');
	$this->redirect('Task:default', $list->id);
}

A ještě si implementujeme metodu createList ve třídě ListRepository

public function createList(string $title)
{
	return $this->getTable()->insert([
		'title' => $title
	]);
}

Povšimněte si přesměrování. Metoda insert() vrací záznam, který byl vložen do databáze. Můžeme tak tedy snadno provést přesměrování na nově vytvořený seznam úkolů.

V šabloně @layout.latte upravíme postranní panel:

<div id="sidebar">
	<div class="task-lists">
		<ul>
			<li n:foreach="$lists as $list"><a n:href="Task: $list->id">{$list->title}</a></li>
		</ul>
	</div>

	<fieldset>
		<legend>Nový seznam</legend>
		{form newListForm}
		<div class="new-list-form">
			{control $form errors}

			{input title}
			{input create}
		</div>
		{/form}
	</fieldset>
</div>

Vytvořili jsme formuláře jak pro vkládání úkolů, tak pro zakládání nových seznamů. Nyní si napíšeme vlastní komponentu.

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