Formulář ve formuláři? HTML to zakazuje, Nette 3.3 ne
Že <form> nesmí být uvnitř jiného
<form>, ví každý, kdo psal složitější stránku.
A pokud jste někdy potřebovali jeden formulář vnořit do druhého, víte,
že čisté řešení dlouho neexistovalo. Nette Forms 3.3 to konečně mění:
{form detached} obejde letité omezení HTML jediným slovem
v šabloně.
Proč nejde formulář ve formuláři
Představte si pokladnu e-shopu: velký formulář s doručovací adresou, dopravou a platbou. A uvnitř malý formulář na slevový kód, který cenu přepočítá sám, aniž by odeslal celou objednávku. Dva nezávislé formuláře, jeden vizuálně uvnitř druhého.
První nápad je napsat to doslova:
<form n:name="orderForm">
{input address}
{* tohle prohlížeč zahodí *}
<form n:name="couponForm">
{input code}
{input apply}
</form>
{input send}
</form>
Jenže HTML vnořování <form> do
<form> nepovoluje a prohlížeč se nezeptá. Při
parsování prostě zahodí vnitřní otevírací značku, takže žádný
couponForm v dokumentu nevznikne. Navíc první
</form> ukončí ten vnější formulář předčasně,
takže i tlačítko send za ním propadne ven. Nedostanete chybu,
dostanete těžko dohledatelný bug.
Atribut form mění
pravidla hry
HTML5 má pro tuhle situaci elegantní východisko: atribut form
na ovládacím prvku. Libovolný <input> nebo
<button> s form="nejakeId" patří
k formuláři <form id="nejakeId"> bez ohledu na to, kde
v dokumentu fyzicky leží.
Formulář a jeho prvky tak můžeme od sebe oddělit:
<form id="newsletter" action="/subscribe" method="post"></form>
<!-- klidně o kus dál, v úplně jiné části stránky -->
<input name="email" form="newsletter">
<button form="newsletter">Odebírat</button>
Značka <form> je prázdná, prvky se na ni odkazují
přes id. Prohlížeč je při odeslání spojí dohromady. Žádný hack, je to
součást specifikace s podporou napříč prohlížeči.
{form detached}:
zanoření na jeden řádek
Přesně tohle teď Nette dělá za vás. Formuláře zůstanou dva nezávislé, každý se svou továrničkou:
protected function createComponentOrderForm(): Form
{
$form = new Form;
$form->addText('address', 'Adresa:');
// ... doprava, platba
$form->addSubmit('send', 'Odeslat objednávku');
return $form;
}
protected function createComponentCouponForm(): Form
{
$form = new Form;
$form->addText('code', 'Slevový kód:');
$form->addSubmit('apply', 'Uplatnit', function (Form $form, $data) {
$this->applyCoupon($data->code);
});
return $form;
}
Drobnost na okraj: addSubmit() v 3.3 přijímá obsluhu
kliknutí rovnou při vytvoření, takže ji nemusíte věšet zvlášť přes
$button->onClick[].
V šabloně je trik v tom, že slovem detached označíme ten
vnější formulář:
{form detached orderForm}
{input address}
{* samostatný formulář vizuálně uvnitř objednávky *}
{form couponForm}
{input code}
{input apply}
{/form}
{input send}
{/form}
Proč vnější? Detached formulář se vzdá své skutečné značky
<form> a své prvky si připojí přes atribut
form. Tím se uvnitř uvolní místo pro plnohodnotný vnitřní
<form>, který už není zanořený do ničeho. Výsledné
HTML proto vypadá takhle:
<form id="frm-orderForm" method="post" action="..."></form>
<input type="text" name="address" form="frm-orderForm">
<form id="frm-couponForm" method="post" action="...">
<input type="text" name="code">
<input type="submit" name="apply">
</form>
<input type="submit" name="send" form="frm-orderForm">
Vnější orderForm je teď prázdná značka na začátku a
jeho prvky se k ní hlásí atributem form="frm-orderForm".
Vnitřní couponForm je naopak úplně běžný formulář, který
už nikdo neobaluje, takže projde prohlížečem bez problémů. Kliknutí na
„Uplatnit“ odešle jen kupón, kliknutí na „Odeslat objednávku“ celou
objednávku.
Id se odvozuje z názvu komponenty, tedy frm- plus název
(frm-orderForm). Když potřebujete konkrétní hodnotu, nastavte
si ji přes $form->getElementPrototype()->id.
Nemusíte přitom dlouho přemýšlet, který formulář takto označit.
Vnější formulář musí být detached vždy, vnitřní může
zůstat běžný. A protože označit detached i vnitřní
ničemu nevadí, nejjednodušší je dát ho prostě oběma. Totéž platí pro
hlubší zanoření: ať vrstvíte formuláře jakkoli, klidně dejte
detached všem.
Stejný princip pohání POST odkazy
Stejný trik s prázdnou <form> a atributem
form možná znáte z jiného doporučení: odkazy spouštějící
akce. Akce měnící stav serveru, jako mazání, nepatří pod GET odkaz, a
tak se v layoutu nechá jeden prázdný formulář a tlačítka se na něj
zavěsí:
<form method="post" id="postForm"></form>
<table>
<tr n:foreach="$posts as $post">
<td>{$post->title}</td>
<td>
<button class="btn btn-link" form="postForm"
formaction="{link delete $post->id}">smazat</button>
</td>
</tr>
</table>
Akci pak zabezpečíte proti spuštění z cizího webu atributem
#[Requires] (u signálů je kontrola původu automatická):
use Nette\Application\Attributes\Requires;
#[Requires(methods: 'POST', sameOrigin: true)]
public function actionDelete(int $id): void
{
$this->facade->deletePost($id);
$this->redirect('default');
}
{form detached} je vlastně tahle ruční technika zabalená do
Nette formuláře. Jednou si ji napíšete ručně pro pár tlačítek, podruhé
ji necháte vygenerovat pro celý formulář.
{form scope}:
vykreslete jen kus formuláře
Kromě detached přibyla v šabloně ještě druhá značka,
{form scope}. Říká prvkům {input} a
{label} uvnitř, ke kterému formuláři nebo kontejneru patří,
ale sama žádnou značku <form> nevykreslí. Hodí se ve
dvou situacích.
První: vykreslujete políčka nějakého kontejneru ve formuláři. Aby se
u každého nemusela opakovat celá cesta jako
{input address-street}, přepne {form scope} kontext
na kontejner a názvy uvnitř se pak píší relativně:
<form n:name="signForm">
{form scope address}
{input street}
{input city}
{/form}
</form>
Druhá: přes AJAX překreslujete jen kousek už vykresleného formuláře,
třeba jediné políčko ve snippetu. Novou značku <form> tu
vykreslit nesmíte, ta na stránce už je, ale {input} pořád
musí vědět, kam patří:
{snippet captcha}
{form scope signForm}
{input captcha}
{/form}
{/snippet}
Nejspíš vám to připomíná dřívější {formContext} a
{formContainer}. A přesně ty scope nahrazuje.
Každá z nich uměla jednu z těch dvou věcí a lidi si je pletli; jedno
klíčové slovo, které se chová podle kontextu, je k zapamatování
příjemnější. Obě staré značky přitom dál fungují.
Závěr
Co HTML zakázalo, vrací atribut form zadními vrátky. Nette
Forms 3.3 z toho dělá jediné slovo v šabloně, detached, a
stejný princip, který drží POST odkazy bezpečné, máte teď i pro
plnohodnotné zanořené formuláře. Stačí upgradnout na Forms 3.3 a Latte
3.1. Novinek je ve verzi víc, třeba modernizovaná
ochrana proti CSRF, která místo tokenu kontroluje hlavičku
Sec-Fetch-Site.
Další čtení
- Čtvrt století s CSRF. Teď ho konečně řeší prohlížeč
- Latte 3.1: Když šablonovací systém skutečně rozumí HTML
- Pět novinek v Latte 3.1, které vám zpříjemní život
- Nové prvky pro vaše formuláře: datum, čas, barvy a čísla
- Nette Http 3.4.0 přináší ochranu proti SSRF útokům
- {linkBase} přináší konzistenci do odkazování
Chcete-li odeslat komentář, přihlaste se