Formuláře

Nette\Forms výrazně usnadňují vytváření a zpracování webových formulářů ve vašich aplikacích. Co všechno umí?

  • validovat odeslaná data na straně serveru i JavaScriptem
  • poskytují zabezpečení proti zranitelnostem
  • zvládají několik režimů vykreslování
  • vícejazyčnost

Nette Framework klade velký důraz na bezpečnost aplikací, a proto úzkostlivě dbá i na dobré zabezpečení formulářů. Dělá to zcela transparentně a nevyžaduje manuálně nic nastavovat. Ochrání vaše aplikace před útokem Cross Site Scripting (XSS)Cross-Site Request Forgery (CSRF), odfiltruje ze vstupů kontrolní znaky, ověří validitu UTF-8 kódování nebo jestli nejsou položky vybrané v select boxech podvržené atd.

Použitím Nette\Forms se vyhneme celé řadě rutinních úkolů, jako je třeba psaní dvojí validace (na straně serveru a klienta), minimalizujeme pravděpodobnost vzniku chyb a bezpečnostních děr.

První formulář

Vytvoříme si v naší aplikaci jednoduchý registrační formulář. Přidáme ho do presenteru pomocí tzv. továrny:

use Nette\Application\UI;

class HomepagePresenter extends UI\Presenter
{

    // ...

    protected function createComponentRegistrationForm()
    {
        $form = new UI\Form;
        $form->addText('name', 'Jméno:');
        $form->addPassword('password', 'Heslo:');
        $form->addSubmit('login', 'Registrovat');
        $form->onSuccess[] = [$this, 'registrationFormSucceeded'];
        return $form;
    }

    // volá se po úspěšném odeslání formuláře
    public function registrationFormSucceeded(UI\Form $form, $values)
    {
        // ...
        $this->flashMessage('Byl jste úspěšně registrován.');
        $this->redirect('Homepage:');
    }
}

v šabloně ho vykreslíme makrem control:

{control registrationForm}

a v prohlížeči bude vypadat takto:

Vytvořili jsme formulář, který po odeslání a úspěšné validaci zavolá metodu registrationFormSucceeded(). Jako první parametr této metody je předán samotný formulář. Do druhého parametru se předávají odeslané hodnoty formuláře v objektu Nette\Utils\ArrayHash. Pokud chceme obdržet místo objektu pole, dáme parametru typehint array $values. Pro získání odeslaných hodnot můžeme také použít funkci $values = $form->getValues($asArray = FALSE).

V rámci životního cyklu presenteru dochází ke zpracování formuláře na stejné úrovni jako zpracování signálů (metody handle*), tedy po action* metodě a před render* metodou.

Vykreslený formulář splňuje základní pravidlo přístupnosti – všechny popisky jsou zapsány jako <label> a provázané s příslušným formulářovým prvkem. Při kliknutí na popisku se kurzor automaticky objeví ve formulářovém políčku.

Data v proměnné $values neobsahují hodnoty formulářových tlačítek, takže je lze obvykle rovnou použít pro další zpracování (například vložení do databáze). Zároveň si můžete všimnout, že z textových políček jsou automaticky odstraněny levo- i pravostranné mezery. Schválně si zkuste do políčka napsat své jméno a za něj několik mezer – po odeslání budou mezery ořezané.

Zmínili jsme se o validaci, ale formulář zatím žádná validační pravidla nemá. Pojďme to napravit. Jméno bude povinné, proto je označíme metodou setRequired(), jejíž volitelný argument je text chybové hlášky, která se zobrazí, pokud uživatel jméno nevyplní:

$form->addText('name', 'Jméno:')
    ->setRequired('Zadejte prosím jméno');

Zkuste si odeslat formulář bez vyplněného jména a uvidíte, že se zobrazí chybová hláška a server vám jej bude nabízet do té doby, dokud jej nevyplníte v souladu s validačními pravidly. Formulář se automaticky validuje na straně klienta i na straně serveru.

Pokud nevycházíte z nette/sandbox, musíte pro zprovoznění JavaScript validace zalinkovat soubor netteForms.js, který najdete ve složce src/assets.

Nette Framework povinným prvkům nastaví CSS třídu required. Zkusme přidat stylopis

<style>
.required label { color: maroon }
</style>

a popiska „Jméno“ bude červená.

Označením povinných prvků validování pochopitelně nekončí. Přidáme další validační pravidla metodou addRule(), jejíž první argument říká, co chceme ověřovat, a druhý argument je opět text hlášky, která se zobrazí, pokud hodnota validací neprojde. Můžeme si vytvářet i vlastní validační pravidla, zatím si však vystačíme s předdefinovanými.

Formulář rozšíříme o nové políčko „věk“ s podmínkou, že je nepovinné, musí to být číslo (Form::INTEGER) a navíc v povoleném rozsahu (Form::RANGE). Zde využijeme třetí parametr metody addRule(), kterým předáme validátoru požadovaný rozsah:

$form->addText('age', 'Věk:')
    ->setRequired(FALSE)
    ->addRule(Form::INTEGER, 'Věk musí být číslo')
    ->addRule(Form::RANGE, 'Věk musí být od 18 do 120', [18, 120]);

Zde vzniká prostor pro drobný refactoring. V chybové hlášce a ve třetím parametru jsou čísla uvedená duplicitně, což není ideální. Pokud bychom tvořili vícejazyčné formuláře a hláška obsahující čísla by se musela přeložit do více jazyků, ztížila by se pozdější změna hodnot. Z toho důvodu je možné použít zástupné znaky v tomto formátu:

->addRule(Form::RANGE, 'Věk musí být od %d do %d let', [18, 120]);

Nette Framework podporuje HTML5 včetně nových formulářových prvků. Díky tomu můžeme políčko pro zadání věku označit jako číselné:

$form->addText('age', 'Věk:')
    ->setType('number')
    ...

V nejpokročilejších prohlížečích, jako je Chrome, Safari nebo Opera, se zobrazí šipečky pro snadnější změnu hodnoty, iPhone zobrazí optimalizovanou klávesnici s číslicemi.

Vraťme se k prvku password, který taktéž učiníme povinným a ještě ověříme minimální délku hesla (Form::MIN_LENGTH), opět s využitím zástupného znaku:

$form->addPassword('password', 'Heslo:')
    ->setRequired('Zvolte si heslo')
    ->addRule(Form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaky', 3);

Přidáme do formuláře ještě políčko passwordVerify, kde uživatel zadá heslo ještě jednou, pro kontrolu. Pomocí validačních pravidel zkontrolujeme, zda jsou obě hesla stejná (Form::EQUAL). Všimněte si dynamické odvolávky na první heslo pomocí hranatých závorek:

$form->addPassword('passwordVerify', 'Heslo pro kontrolu:')
    ->setRequired('Zadejte prosím heslo ještě jednou pro kontrolu')
    ->addRule(Form::EQUAL, 'Hesla se neshodují', $form['password']);

Pokud by formulář nesloužil k registraci nových uživatelů, ale pro editaci záznamů, hodilo by se na začátku nastavit prvkům výchozí hodnoty.

Tímto máme hotový plně funkční formulář, který disponuje validací na straně klienta (tj. JavaScriptovou validací) i validací na straně serveru. Automaticky ošetřuje magic quotes, ověřuje, zda útočník neposílá nevalidní UTF-8 řetězce apod. Na tyto věci nemusíme myslet.

Příklady si můžete stáhnout. Zkuste si do něj přidat i další prvky popsané níže. Inspiraci najdete také v distribuci v adresáři examples/Forms.

Formulářové prvky

Přehled standardních formulářových prvků.

addText($name, $label = NULL)

Přidá jednořádkové textové políčko (třída TextInput). Automaticky ořezává levo- a pravostranné mezery nebo případné odřádkování. Kromě standardních validačních pravidel lze použít navíc i tato:

Form::MIN_LENGTH minimální délka textu
Form::MAX_LENGTH maximální délka textu
Form::LENGTH délka v daném rozsahu nebo právě tato délka
Form::EMAIL je hodnota platná e-mailová adresa?
Form::URL je hodnota absolutní URL?
Form::PATTERN testuje oproti regulárnímu výrazu celou hodnotu, tj. jako by byl obalen znaky ^ a $
Form::INTEGER je hodnota celočíselná?
Form::NUMERIC alias pro Form::INTEGER
Form::FLOAT je hodnota číslo?
Form::MIN minimální hodnota číselného prvku
Form::MAX maximální hodnota číselného prvku
Form::RANGE je hodnota v daném rozsahu?
$form->addText('zip', 'PSČ:')
    ->setRequired()
    ->addRule(Form::PATTERN, 'PSČ musí mít 5 číslic', '([0-9]\s*){5}');

Validační pravidla Form::INTEGER, NUMERIC a FLOAT rovnou převádí hodnotu na integer resp. float. A dále pravidlo Form::URL, které akceptuje i řetězec ve tvaru např. nette.org, jej automaticky doplní na plnohodnotné https://nette.org.

addPassword($name, $label = NULL)

Přidá políčko pro zadání hesla (třída TextInput). Automaticky ořezává levo- a pravostranné mezery nebo případné odřádkování. Při znovu-zobrazení formuláře bude políčko prázdné. Lze použít stejná validační pravidla jako pro addText.

$form->addPassword('password', 'Heslo:')
    ->setRequired()
    ->addRule(Form::MIN_LENGTH, 'Heslo musí mít alespoň %d znaky', 3)
    ->addRule(Form::PATTERN, 'Musí obsahovat číslici', '.*[0-9].*');

addTextArea($name, $label = NULL)

Přidá pole pro zadání víceřádkového textu (třída TextArea). Lze použít stejná validační pravidla jako pro addText. Na rozdíl od jednořádkového vstupního políčka k žádnému ořezávání mezer nedochází.

$form->addTextArea('note', 'Poznámka:')
    ->setRequired(FALSE) // nepovinná
    ->addRule(Form::MAX_LENGTH, 'Poznámka je příliš dlouhá', 10000);

addEmail($name, $label = NULL)

Přidá políčko pro zadání e-mailové adresy s kontrolou její platnosti (třída TextInput). Lze použít stejná validační pravidla jako pro addText.

$form->addEmail('email', 'E-mail:');

addUpload($name, $label = NULL)

Přidá políčko pro upload souboru (třída UploadControl). Kromě standardních validačních pravidel lze použít navíc i tato:

Form::MAX_FILE_SIZE ověřuje maximální velikost souboru
Form::MIME_TYPE ověření MIME type
Form::IMAGE ověření, že jde o obrázek JPEG, PNG nebo GIF
$form->addUpload('avatar', 'Avatar:')
    ->setRequired(FALSE) // nepovinný
    ->addRule(Form::IMAGE, 'Avatar musí být JPEG, PNG nebo GIF.')
    ->addRule(Form::MAX_FILE_SIZE, 'Maximální velikost souboru je 64 kB.', 64 * 1024 /* v bytech */);
$form->addUpload('avatar', 'Avatar:', TRUE);

addMultiUpload($name, $label = NULL)

Přidá políčko, které uživateli umožní nahrát více souborů najednou. Validační pravidla jsou stejné, jako u addUpload() a přidává následující pravidla:

Form::MIN_LENGTH minimální počet souborů
Form::MAX_LENGTH maximální počet souborů
Form::LENGTH počet souborů v daném rozsahu nebo právě tato délka
$form->addMultiUpload('files', 'Soubory');

addHidden($name, $default = NULL)

Přidá skryté pole (třída HiddenField).

$form->addHidden('userid');

addCheckbox($name, $caption = NULL)

Přidá zaškrtávací políčko (třída Checkbox). Políčko vrací hodnotu buď TRUE nebo FALSE, podle toho, zda je zaškrtnuté či nikoliv.

$form->addCheckbox('agree', 'Souhlasím s podmínkami')
    ->setRequired('Je potřeba souhlasit s podmínkami');

addRadioList($name, $label = NULL, array $items = NULL)

Přidá přepínací tlačítka (třída RadioList). Pole nabízených hodnot předáme jako třetí parametr.

$sex = [
    'm' => 'muž',
    'f' => 'žena',
];
$form->addRadioList('gender', 'Pohlaví:', $sex);

// pro vypsání možností do 1 řádku
$form->addRadioList('gender', 'Pohlaví:', $sex)
    ->getSeparatorPrototype()->setName(NULL);

addCheckboxList($name, $label = NULL, array $items = NULL)

Přidá seznam zaškrtávacích políček pro výběr více prvků. Pole nabízených hodnot opět předáme jako třetí parametr. Stejně jako v případě selectboxů nebo radiolistů kontroluje, zda odeslané hodnoty jsou z těch, které nabízíme.

$form = new Form;
$form->addCheckboxList('colors', 'Barvy:', [
    'r' => 'červená',
    'g' => 'zelená',
    'b' => 'modrá',
]);

addSelect($name, $label = NULL, array $items = NULL)

Přidá select box (třída SelectBox). U select boxů má často první položka speciální význam, slouží jako výzva k akci. K přidání takové položky slouží metoda setPrompt(). Pole nabízených hodnot předáme jako třetí parametr. Pole může být i dvourozměrné:

$countries = [
    'Europe' => [
        'CZ' => 'Česká Republika',
        'SK' => 'Slovensko',
        'GB' => 'Velká Británie',
    ],
    'CA' => 'Kanada',
    'US' => 'USA',
    '?'  => 'jiná',
];

$form->addSelect('country', 'Země:', $countries)
    ->setPrompt('Zvolte zemi');

Prvky je rovněž možné přidat pomocí metody setItems(). Pokud chceme místo klíčů položek získat přímo jejich hodnoty, můžeme toho docílit druhým argumentem:

$form->addSelect('country', 'Země:')
    ->setItems($countries, FALSE);

addMultiSelect($name, $label = NULL, array $items = NULL)

Přidá select box pro výběr více položek (třída MultiSelectBox).

$form->addMultiSelect('options', 'Možnosti:', $options);

addSubmit($name, $caption = NULL)

Přidá odesílací tlačítko (třída SubmitButton).

$form->addSubmit('submit', 'Odeslat');

addButton($name, $caption)

Přidá tlačítko (třída Button), které nemá odesílací funkci. Lze ho tedy využít na nějakou jinou funkci, např. zavolání js funkce při kliknutí.

$form->addButton('raise', 'Zvýšit plat')
    ->setAttribute('onclick', 'raiseSalary()');

addImage($name, $alt = NULL)

Přidá odesílací tlačítko v podobě obrázku (třída ImageButton).

$form->addImage('submit', '/path/to/image');

addContainer($name)

Přidá pod-formulář (třída Container), nebo-li kontejner, do kterého lze přidávat další prvky stejným způsobem, jako je přidáváme do formuláře. Fungují i metody setDefaults() nebo getValues().

$sub1 = $form->addContainer('first');
$sub1->addText('name', 'Your name:');
$sub1->addEmail('email', 'Email:');

$sub2 = $form->addContainer('second');
$sub2->addText('name', 'Your name:');
$sub2->addEmail('email', 'Email:');

Low-level formuláře

Od verze 2.1 lze používat i prvky, které zapíšeme pouze v šabloně a nepřidáme je do formuláře některou z metod $form->addXyz(). Když například vypisujeme záznamy z databáze a dopředu nevíme, kolik jich bude a jaké budou mít ID, a chceme u každého řádku zobrazit checkbox nebo radio button, stačí jej nakódovat v šabloně:

{foreach $items as $item}
    <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p>
{/foreach}

A po odeslání hodnotu zjistíme:

$values = $form->getHttpData($form::DATA_TEXT, 'sel[]');
$values = $form->getHttpData($form::DATA_TEXT | $form::DATA_KEYS, 'sel[]');

kde první parametr je typ elementu (DATA_FILE pro type=file, DATA_LINE pro jednořádkové vstupy jako text, password, email apod. a DATA_TEXT pro všechny ostatní) a druhý parametr sel[] odpovídá HTML atributu name. Typ elementu můžeme kombinovat s hodnotou DATA_KEYS, která zachová klíče prvků. To se hodí zejména pro select, radioList a checkboxList.

Podstatné je, že getHttpData() vrací sanitizovanou hodnotu, v tomto případě to bude vždy pole validních UTF-8 řetězců, ať už se pokusíte serveru podstrčit cokoliv. Jde o obdobu přímé práce s $_POST nebo $_GET avšak s tím podstatným rozdílem, že vždy vrací čistá data, tak, jak jste zvyklí u standardních prvků Nette formulářů.

Validace

Na již zmíněné prvky lze použít tyto validační pravidla:

Form::FILLED je prvek vyplněn?
Form::REQUIRED alias pro Form::FILLED
Form::EQUAL je hodnota rovna uvedené?
Form::NOT_EQUAL prvek se nesmí rovnat zadané hodnotě
Form::IS_IN testuje, zda hodnota spadá do výčtu
Form::IS_NOT_IN hodnota nesmí spadat do výčtu
Form::VALID je prvek vyplněn správně?
Form::BLANK prvek nesmí být vyplněn

Všem validačním pravidlům můžeme přidat vlastní chybovou hlášku, nebo se použije hláška výchozí, kterou můžete případně přepsat. U vícejazyčných formulářů se hlášky automaticky překládají.

V textu chybových hlášek lze používat i speciální zástupné řetězce:

%label nahradí se textem popisky
%name nahradí se identifikátorem prvku
%value nahradí se zadanou hodnotou

Kromě pravidel lze přidávat také dotazovací podmínky. Ty se zapisují podobně jako pravidla, jen místo addRule() použijeme metodu addCondition() a pochopitelně neuvádíme žádnou chybovou zprávu (podmínka se jen ptá):

$form->addPassword('password', 'Heslo:')
    // pokud není heslo delší než 5 znaků
    ->addCondition(Form::MAX_LENGTH, 5)
        // pak bude muset obsahovat číslici
        ->addRule(Form::PATTERN, 'Musí obsahovat číslici', '.*[0-9].*');

Podmínku je možné vázat i na jiný prvek, než ten aktuální. Stačí addCondition() nahradit za addConditionOn() a jako první parametr uvést odvolávku na jiný prvek. V tomto případě se bude e-mail vyžadovat tehdy, zaškrtne-li se checkbox (tj. jeho logická hodnota bude TRUE):

$form->addCheckbox('newsletters', 'zasílejte mi newslettery');

$form->addEmail('email', 'E-mail:')
    // pokud je checkbox zaškrtnut
    ->addConditionOn($form['newsletters'], Form::EQUAL, TRUE)
        // pak vyžaduj e-mail
        ->setRequired('Zadejte e-mailovou adresu');

Podmínky je možné negovat znakem ~ (vlnovka), tj. addCondition(~Form::NUMBER, ...). Také lze z podmínek vytvářet komplexní struktury za pomoci metod elseCondition() a endCondition().

Jak vidíte, jazyk pro formulování podmínek a pravidel je velice silný. Všechny konstrukce přitom fungují jak na straně serveru, tak i na straně JavaScriptu.

Můžeme si také přidat vlastní validátory. Metody addRule() a addCondition() totiž jako název pravidla akceptují callback:

// uživatelský validátor: testuje, zda je hodnota dělitelná argumentem
// poznámka: toto je skutečná funkce, nikoliv metoda v presenteru
function divisibilityValidator($item, $arg)
{
    return $item->value % $arg === 0;
}

$form->addInteger('number', 'Číslo:')
    ->addRule('divisibilityValidator', 'Číslo musí být dělitelné %d.', 8);

Vlastní chyby

V mnoha případech se o chybě dozvíme až ve chvíli, kdy zpracováváme platný formulář, například zapisujeme novou položku do databáze a narazíme na duplicitu klíčů. V takovém případě chybu zpětně předáme do formuláře metodou addError(). Tu lze volat buď na konkrétním prvku, nebo přímo na formuláři:

try {
    $values = $form->getValues();
    $this->user->login($values->username, $values->password);
    $this->redirect('Homepage:');

} catch (Nette\Security\AuthenticationException $e) {
    if ($e->getCode() === Nette\Security\IAuthenticator::INVALID_CREDENTIAL) {
        $form->addError('Neplatné heslo.');
    }
    ...
}

Vlastní validační funkce

Pokud z nějakého důvodu potřebujete přidat validační funkcionalitu, typicky ověření správné kombinace hodnot ve více prvcích formuláře, můžete si napsat vlastní validační funkce. Ty pak na formulář navážete pomocí události onValidate:

protected function createComponentSignInForm()
{
    $form = new Form();
    ...
    $form->onValidate[] = [$this, 'validateSignInForm'];
    return $form;
}

public function validateSignInForm($form)
{
    $values = $form->getValues();

    if (...) { // validační podmínka
        $form->addError('Tato kombinace není možná.');
    }
}

Na událost onValidate můžete registrovat libovolný počet funkcí. Funkce je chápána jako úspěšná, pokud nepřidá do formuláře chybu pomocí $form->addError().

JavaScript

Validační pravidla se na stranu JavaScriptu přenášejí v HTML atributech data-nette-rules, které obsahují JSON popisující jednotlivá pravidla nebo podmínky. Samotnou validaci pak provádí skript, který odchytí událost submit, projde jednotlivé prvky a vykoná příslušnou validaci. Výchozí implementací je soubor netteForms.js, který najdete v distribuci v adresáři src/assets. Stačí jej tedy do stránky zalinkovat přes <script src="netteForms.js"></script>.

Vlastní validační pravidla přidáme rozšířením objektu Nette.validators:

<script>
Nette.validators.divisibilityValidator = function(elem, args, val) {
    return val % args === 0;
};
</script>

Pokud náš validační callback v PHP je statická metoda ve třídě, tak název pro JavaScriptový validátor vytvoříme smazáním zpětných lomítek \ a nahrazením dvojité dvojtečky za jedno podtržítko _, např. App\MyValidator::divisibilityValidator zapíšeme jako AppMyValidator_divisibilityValidator.

Vypnutí validace

Někdy se může hodit validaci vypnout. Pokud stisknutí odesílacího tlačítka nemá provádět validaci (vhodné pro tlačítka Cancel nebo Preview), vypneme ji metodou $submit->setValidationScope([]). Pokud má provádět validaci jen částečnou, můžeme určit které pole nebo formulářové kontejnery se mají validovat.

$form->addText('name')->setRequired();

$details = $form->addContainer('details');
$details->addInteger('age')->setRequired('age');
$details->addInteger('age2')->setRequired('age2');

$form->addSubmit('send1'); // Validuje celý formuláře
$form->addSubmit('send2')->setValidationScope(FALSE); // Nevaliduje vůbec
$form->addSubmit('send3')->setValidationScope([$form['name']]); // Validuje pouze pole name
$form->addSubmit('send4')->setValidationScope([$form['details']['age']]); // Validuje pouze pole age
$form->addSubmit('send5')->setValidationScope([$form['details']]); // Validuje kontejner details

setValidationScope neovlivní událost onValidate u formuláře, která bude zavolána vždy. Událost onValidate u kontejneru bude vyvolána pouze pokud je tento kontejner označen pro částečnou validaci.

Zpracování hodnot

K hodnotám formuláře se dostaneme pomocí metody getValues(), která nám vrací objekt typu ArrayHash nebo pole, uvedeme-li TRUE jako první parametr.

$values = $form->getValues();     // Nette\Utils\ArrayHash
$values = $form->getValues(TRUE); // array

Deaktivace prvků

Pokud chceme některý prvek deaktivovat, můžeme využít metodu $control->setDisabled(TRUE)

$form->addEmail('email', 'E-mail:')->setDisabled(TRUE);

Do tohoto prvku nepůjde zapisovat a jeho hodnota nebude obsažena v datech vracených funkcí $form->getValues().

Pokud chceme prvek použít jen pro čtení, tj. aby se nastavená hodnota prvku zobrazila, ale prvek byl neaktivní, je potřeba nejdřív prvek deaktivovat a poté mu nastavit hodnotu. Je to z toho důvodu, že metoda setDisabled() hodnotu prvku vynuluje.

$form->addText('readonly', 'Readonly:')->setDisabled()->setValue('readonly value');

Pokud potřebujeme prvek pouze vyjmout z těchto dat, použijeme funkci $control->setOmitted(TRUE). To se hodí pro různá hesla pro kontrolu, antispamové prvky atd.

$form->addText('antispam', 'Antispam:')->setOmitted(TRUE);

Úprava hodnot

Pomocí metody addFilter můžeme upravit hodnotu ještě před samotným zpracování formuláře. addFilter je možno kombinovat s metodami addCondition a addConditionOn.

$form->addText('zip', 'PSČ:')
    ->addCondition($form::FILLED)
    ->addFilter(function ($value) {
        return str_replace(' ', '', $value);
    });

Když poté přístoupíme ve zpracování formuláře k jeho hodnotám, PSČ již bude zbaveno mezer.

Další rozšíření prvků

Formulářové prvky můžeme kromě podmínek a pravidel rozšířit o popisek, upravit class apod.

Pozor na pořadí podmínek, pravidel a užití těchto rozšíření.

Nastavení třídy nebo JavaScriptu:

$form->addInteger('number', 'Číslo:')
    ->setAttribute('class', 'bigNumbers');

$form->addSelect('rank', 'Řazení dle:', ['ceny', 'názvu'])
    ->setAttribute('onchange', 'submit()'); // při změně odeslat


// chceme-li to samé udělat pro celý $form
$form->getElementPrototype()->id = 'myForm';
$form->getElementPrototype()->target = '_top';

Nastavení typu (např. pro HTML5):

$form->addText('email', 'Váš email:')
    ->setType('email')
    ->setAttribute('placeholder', 'napište email');

Nastavení popisku (defaultně se vypisuje za polem):

$form->addText('phone', 'Číslo:')
    ->setOption('description', 'Toto číslo zůstane skryté');

Pokud chceme umístit HTML obsah, využijeme třídy Html

use Nette\Utils\Html;

$form->addText('phone', 'Číslo:')
    ->setOption('description', Html::el('p')
        ->setHtml('Toto číslo zůstane skryté. <a href="...">Podmínky uchovávání Vašeho čísla</a>')
    );

Html prvek lze využít i místo labelu: $form->addCheckbox('conditions', $label).

Formuláře v presenterech

V presenterech se místo třídy Nette\Forms\Form používá od ní odvozená třída Nette\Application\UI\Form.

Použití stejného formuláře ve více presenterech

Pokud potřebujete jeden formulář použít ve více presenterech, máte dvě možnosti:

  1. vložit do hierarchie presenterů jejich společného předka a továrnu definovat tam
  2. nebo definovat formulář v samostatné tovární třídě a v jednotlivých továrnách vytvářet jeho instance.

Vhodné umístění pro takovou třídu je např. app/forms/SignInFormFactory.php. Naše tovární třída bude vypadat takto:

use Nette\Application\UI\Form;

class SignInFormFactory
{
    /**
     * @return Form
     */
    public function create()
    {
        $form = new Form;

        $form->addText('name', 'Jméno:');
        // ...
        $form->addSubmit('login', 'Přihlásit se');

        return $form;
    }
}

V továrničce každého presenteru, který náš formulář používá, jej následně vytvoříme voláním metody create():

protected function createComponentSignInForm()
{
    $form = (new SignInFormFactory())->create();
    $form['login']->caption = 'Pokračovat'; // můžeme také formulář pozměnit

    $form->onSuccess[] = [$this, 'signInFormSubmitted']; // a přidat událost po odeslání

    return $form;
}

Odeslaný formulář ale můžeme také zpracovávat na jediném místě. Do definice formuláře přesuneme volání událostí i s metodou signInFormSubmitted a přejmenujeme ji například na submitted, případně použijeme anonymní funkci:

use Nette\Application\UI\Form;

class SignInFormFactory
{
    /**
     * @return Form
     */
    public function create()
    {
        $form = new Form;

        $form->addText('name', 'Jméno:');
        ...
        $form->addSubmit('login', 'Přihlásit se');

        $form->onSuccess[] = function (Form $form, \stdClass $values) {
            // zde provedeme zpracování formuláře
        };

        return $form;
    }
}

Odesílání formuláře

Pokud má formulář více než jedno tlačítko, mezi kterými chceme rozlišovat, je vhodnější nastavit handler na událost onClick tlačítka. Ten se volá před handlerem události onSuccess:

$form->addSubmit('login', 'Přihlásit se')
    ->onClick[] = [$this, 'signInFormSubmitted'];

Když se formulář odešle tlačítkem enter, za odesílací tlačítko se považuje to první.

Handlery událostí onSuccess a onClick se volají pouze v případě, že je odeslání validní. Uvnitř obslužných metod tedy nemusíme validitu ověřovat. Formulář má ještě událost onSubmit, která se volá vždy nezávisle na validitě.

Výchozí hodnoty

Nastavit výchozí hodnoty lze dvěma způsoby. Metodou setDefaults() nad celým formulářem nebo kontejnerem:

$form->addText('name', 'Jméno');
$form->addInteger('age', 'Věk');

$form->setDefaults([
    'name' => 'John',
    'age' => '33'
]);

nebo metodou setDefaultValue() nad prvkem:

$form->addEmail('email', 'E-mail')
    ->setDefaultValue('user@example.com');

U SelectBoxu nebo RadioListu zadáme jako výchozí hodnotu klíč z předaného pole hodnot:

$form->addSelect('country', 'Country', [
    'cz' => 'Česká republika',
    'sk' => 'Slovensko',
]);
$form['country']->setDefaultValue('sk');

U CheckBoxu:

$form->addCheckbox('agree', 'Agree with conditions')
    ->setDefaultValue(TRUE);

Další užitečnou možností je použití „emptyValue“. Pokud je hodnota prvku po odeslání formuláře shodná s nastavenou „emptyValue“, tváří se prvek jako nevyplňený.

$form->addText('phone', 'Phone:')
    ->setEmptyValue('+42');

Vzhled formulářů

Vzhled formulářů může být velmi různorodý. V praxi můžeme narazit na dva extrémy. Na jedné straně stojí potřeba v aplikaci vykreslovat řadu formulářů, které jsou si vizuálně podobné jako vejce vejci, a oceníme snadné vykreslení pomocí echo $form. Jde obvykle o případ administračních rozhraní.

Na druhé straně tu jsou rozmanité formuláře, kde platí: co kus, to originál. Jejich podobu nejlépe popíšeme jazykem HTML. A samozřejmě kromě obou zmíněných extrémů narazíme na spoustu formulářů, které se pohybují někde mezi.

Výchozí DefaultFormRenderer

Automatické vykreslení formuláře obstarává tzv. renderer. Ten lze nastavit metodou setRenderer. Předá se mu řízení při zavolání metody $form->render() nebo echo $form. Pokud nenastavíme vlastní renderer, bude použit výchozí vykreslovač Nette\Forms\Rendering\DefaultFormRenderer. Stačí tedy napsat:

echo $form;

a formulář je na světě. Prvky formuláře se vykreslí do HTML tabulky. Výstup vypadá takto:

<table>
<tr class="required">
    <th><label class="required" for="frm-name">Jméno:</label></th>

    <td><input type="text" class="text" name="name" id="frm-name" value="" /></td>
</tr>

<tr class="required">
    <th><label class="required" for="frm-age">Věk:</label></th>

    <td><input type="text" class="text" name="age" id="frm-age" value="" /></td>
</tr>

<tr>
    <th><label>Pohlaví:</label></th>
    ...

Hezky naformátované, viďte? :-)

Zda použít nebo nepoužít pro kostru formuláře tabulku je sporné a řada webdesignerů preferuje jiný markup. Například definiční seznam. Překonfigurujeme DefaultFormRenderer tak, aby formulář v podobě seznamu vykreslil. Konfigurace se provádí editací pole $wrappers. První index vždy představuje oblast a druhý její atribut. Jednotlivé oblasti znázorňuje obrázek:

Standardně je skupina prvků controls obalena tabulkou <table>, každý pair představuje řádek tabulky <tr> a dvojice label a control jsou buňky <th> a <td>. Nyní obalující elementy změníme. Oblast controls vložíme do kontejneru <dl>, oblast pair necháme bez kontejneru, label vložíme do <dt> a nakonec control obalíme značkami <dd>:

$renderer = $form->getRenderer();
$renderer->wrappers['controls']['container'] = 'dl';
$renderer->wrappers['pair']['container'] = NULL;
$renderer->wrappers['label']['container'] = 'dt';
$renderer->wrappers['control']['container'] = 'dd';

echo $form;

Výsledkem je tento HTML kód:

<dl>
    <dt><label class="required" for="frm-name">Jméno:</label></dt>

    <dd><input type="text" class="text" name="name" id="frm-name" value="" /></dd>


    <dt><label class="required" for="frm-age">Věk:</label></dt>

    <dd><input type="text" class="text" name="age" id="frm-age" value="" /></dd>


    <dt><label>Pohlaví:</label></dt>
    ...
</dl>

V poli wrappers lze ovlivnit celou řadu dalších atributů:

  • přidávat CSS třídy jednotlivým typům formulářových prvků
  • rozlišovat CSS třídou liché a sudé řádky
  • vizuálně odlišit povinné a volitelné položky
  • určovat, zda se chybové zprávy zobrazí přímo u prvků nebo nad formulářem

Podpora pro Bootstrap

V příkladech najdete ukázky, jak nakonfigurovat vykreslování formulářů pro Twitter Bootstrap 2 a Bootstrap 3

Manuální vykreslování

Formuláře lze vykreslovat i ručně a tím získat větší kontrolu nad kódem. Celý formulář umístíme mezi párové makra {form myForm} a {/form}. Jednotlivé prvky můžeme vypisovat pomocí maker {input myInput}, které vypíše formulářový prvek, a {label myLabel /}, které vypíše jeho popisek.

{form signForm}

<!-- Jednoduché vykreslení chyb -->
<ul class="errors" n:if="$form->hasErrors()">
        <li n:foreach="$form->errors as $error">{$error}</li>
</ul>

<table>
<tr class="required">
    <th>{label name /}</th>
    <td>{input name}</td>
</tr>

<!-- V případě že potřebujeme vykreslit ručně jednotlivé prvky radiolistu -->
<p>{input radioList:itemKey} | {input radioList:itemKeyTwo}</p>

...

</table>
{/form}

Velmi snadno také můžete propojit formulář s existující šablonou. Stačí jen doplnit atributy n:name:

function createComponentSignInForm()
{
    $form = new Form;
    $form->addText('user')->setRequired();
    $form->addPassword('password')->setRequired();
    $form->addSubmit('send');
    return $form;
}
<form n:name=signInForm class=form>
    <p><label n:name=user>Username: <input n:name=user size=20></label>
    <p><label n:name=password>Password: <input n:name=password></label>
    <p><input n:name=send class="btn btn-default">
</form>

Atribut n:name lze používat i s elementy <select>, <button> nebo <textarea> a vnitřní obsah se automaticky doplní.

Dále můžete vykreslovat prvky jako je RadioList, Checkbox nebo CheckboxList pěkně po jednotlivých HTML elementech. Říká se tomu partial rendering:

{foreach $form[gender]->items as $key => $label}
    <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label>
{/foreach}

Nebo lze použít klasická makra {input gender:$key} a {label gender:$key}, trik je tom názvu s dvojtečkou. Pro jednoduchý checkbox použijte {input myCheckbox:}.

S vykreslením prvků uvnitř formulářového kontejneru pomůže makro formContainer.

{form signForm}
<table>
    <th>Přeji si emailem odebírat tyto novinky:</th>
    <td>
        {formContainer emailNews}
        <ul>
            <li>{input sport} {label sport /}</li>
            <li>{input science} {label science /}</li>
        </ul>
        {/formContainer}
    </td>
    ...
</table>
{/form}

Jak nastavit HTML elementům další atributy? Metody getControl() a getLabel() vrací element v podobě Nette\Utils\Html objektu, se kterým se dá snadno pracovat. Takto například v Latte:

{form signForm class => 'big'}
<table>
<tr class="required">
    <th>{label name /}</th>
    <td>{input name cols => 40, autofocus => TRUE}</td>
</tr>

Seskupování prvků

Prvky lze seskupovat do vizuálních skupin (fieldsetů) vytvořením skupiny:

$form->addGroup('Personal data');

Po vytvoření nové skupiny se tato stává aktivní a každý nově přidaný prvek je zároveň přidán i do ní. Takže formulář lze stavět tímto způsobem:

$form = new Form;
$form->addGroup('Personal data');
$form->addText('name', 'Your name:');
$form->addInteger('age', 'Your age:');
$form->addEmail('email', 'Email:');

$form->addGroup('Shipping address');
$form->addCheckbox('send', 'Ship to address');
$form->addText('street', 'Street:');
$form->addText('city', 'City:');
$form->addSelect('country', 'Country:', $countries);

Obrana před Cross-Site Request Forgery (CSRF)

Nette Framework ochrání vaše aplikace před útokem Cross-Site Request Forgery (CSRF). Útok spočívá v tom, že útočník naláká oběť na stránku, která nenápadně vykoná požadavek na server, na kterém je oběť přihlášena a server se domnívá, že požadavek vykonala oběť o své vůli.

Ochrana je velmi snadná:

$form->addProtection('Vypršel časový limit, odešlete formulář znovu');

Proti útoku se lze bránit generováním a ověřováním autorizačního tokenu. Ten má platnost po dobu existence session. Díky tomu nebrání použití ve více oknech najednou (v rámci jedné session). Platnost je však možné zkrátit na počet sekund, které se uvedou jako druhý parametr. První parametr je přitom text chybové hlášky, která se zobrazí uživateli, pokud token vypršel.

Obrana by měla být aktivována pro všechny formuláře, které mění citlivá data v aplikaci.

Vícejazyčné formuláře

Pokud programujete vícejazyčnou aplikaci, budete nejspíš potřebovat stejný formulář vykreslit v různých jazykových mutacích. Formuláře v Nette Framework disponují podporou pro snadný překlad. Stačí, když formuláři nastavíte tzv. překladač, což je objekt implementující rozhraní Nette\Localization\ITranslator. Rozhraní má jedinou metodu translate().

class MyTranslator implements Nette\Localization\ITranslator
{
    /**
     * Translates the given string.
     * @param  string   message
     * @param  int      plural count
     * @return string
     */
    public function translate($message, $count = NULL)
    {
        return ...;
    }
}

$form->setTranslator($translator);

V tu chvíli se nejen všechny popisky, chybové hlášky nebo položky select boxů transparentně přeloží do jiného jazyka.

Samostatné použití Nette\Forms

Pokud z nějakého důvodu nechcete používat celý framework, můžete využít Nette/Forms samostatně. Nainstalujete je pomocí Composeru.

composer require nette/forms

Vytvoření formuláře potom vypadá asi takto:

use Nette\Forms\Form;

$form = new Form;

$form->addText('name', 'Jméno:');
$form->addPassword('password', 'Heslo:');
$form->addSubmit('send', 'Registrovat');

echo $form; // vykreslí formulář

Takto vytvořený formulář se metodou POST odešle na stejnou stránku. To se dá snadno změnit:

$form = new Form;
$form->setAction('/submit.php');
$form->setMethod('GET');
...

Teď formulář oživíme. Dotazem na $form->isSuccess() zjistíme, zda byl formulář odeslán a zda byl vyplněn korektně. Pokud bude formulář správně vyplněn, data vypíšeme do okna prohlížeče. Za definici formuláře tedy vložíme kód:

if ($form->isSuccess()) {
    echo 'Formulář byl správně vyplněn a odeslán';

    $values = $form->getValues();
    dump($values);
}

K jednotlivým prvkům formuláře $form lze přistupovat pomocí hranatých závorek, podobně jako k prvkům pole. Takže třeba pod $form['name'] se skrývá objekt Nette\Forms\Controls\TextInput představující první políčko formuláře.

Po odeslání a zpracování formuláře je vhodné přesměrovat na další stránku. Zabrání se tak nechtěnému opětovnému odeslání formuláře tlačítkem Obnovit nebo Zpět.

Vykreslení formuláře

Každý prvek disponuje metodami getLabel() a getControl(), které vracejí HTML kód popisky a samotného prvku. Nette Framework dovoluje ke getterům přistupovat podobně, jako by to byly proměnné, takže stačí psát jen label a control.

<?php $form->render('begin') ?>
<?php $form->render('errors') ?>

<table>
<tr class="required">
    <th><?php echo $form['name']->label // Zavolá getLabel() ?></th>
    <td><?php echo $form['name']->control // Zavolá getControl() ?></td>
</tr>

<tr class="required">
    <th><?php echo $form['age']->label ?></th>
    <td><?php echo $form['age']->control ?></td>
</tr>

...

</table>

<?php $form->render('end') ?>

Rozšiřující metody

Potřebujete formulářům přidat novou metodu? Pak vám pomohou extension method (rozšiřující metody):

Nette\Forms\Container::extensionMethod('addZip', function ($form, $name, $label = NULL) {
    return $form->addText($name, $label)
        ->setRequired(FALSE)
        ->addRule($form::PATTERN, '[0-9]{5}', 'Alespon 5 cisel');
});

$form = new Form;
$form->addZip('zip', 'ZIP code:');