A form inside a form? HTML says no, Nette 3.3 says yes
Anyone who has built a non-trivial page knows a <form>
can't sit inside another <form>. And if you have ever needed
to nest one form in another, you know there was no clean way to do it for a long
time. Nette Forms 3.3 finally changes that: {form detached} works
around the longstanding HTML restriction with a single word in your
template.
Why you can't put a form inside a form
Picture an e-shop checkout: a big form with the delivery address, shipping method and payment. And inside it a small form for a discount code that recalculates the price on its own, without submitting the whole order. Two independent forms, one visually inside the other.
The first instinct is to write it literally:
<form n:name="orderForm">
{input address}
{* the browser will throw this away *}
<form n:name="couponForm">
{input code}
{input apply}
</form>
{input send}
</form>
But HTML doesn't allow nesting <form> inside
<form>, and the browser won't warn you. While parsing it
simply drops the inner opening tag, so no couponForm ever exists in
the document. Worse, the first </form> closes the outer form
early, so even the send button falls out of it. You get no error,
just a bug that is hard to track down.
The form
attribute changes the rules
HTML5 has an elegant way out: the form attribute on a control.
Any <input> or <button> with
form="someId" belongs to the <form id="someId">
no matter where it physically sits in the document.
So we can separate a form from its controls:
<form id="newsletter" action="/subscribe" method="post"></form>
<!-- somewhere else entirely, in a different part of the page -->
<input name="email" form="newsletter">
<button form="newsletter">Subscribe</button>
The <form> tag is empty, the controls reference it by id.
The browser joins them on submit. No hack, it is part of the spec with broad
browser support.
{form detached}:
nesting in one line
This is exactly what Nette now does for you. The two forms stay independent, each with its own factory:
protected function createComponentOrderForm(): Form
{
$form = new Form;
$form->addText('address', 'Address:');
// ... delivery, payment
$form->addSubmit('send', 'Place order');
return $form;
}
protected function createComponentCouponForm(): Form
{
$form = new Form;
$form->addText('code', 'Discount code:');
$form->addSubmit('apply', 'Apply', function (Form $form, $data) {
$this->applyCoupon($data->code);
});
return $form;
}
A small aside: in 3.3 addSubmit() accepts the click handler
right at creation, so you no longer attach it separately via
$button->onClick[].
In the template the trick is to mark the outer form with the word
detached:
{form detached orderForm}
{input address}
{* a standalone form, visually inside the order *}
{form couponForm}
{input code}
{input apply}
{/form}
{input send}
{/form}
Why the outer one? A detached form gives up its real
<form> tag and links its controls back through the
form attribute. That frees up room inside for a full-fledged inner
<form> that is no longer nested in anything. The resulting
HTML looks like this:
<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">
The outer orderForm is now an empty tag at the top and its
controls report to it through form="frm-orderForm". The inner
couponForm, on the other hand, is a perfectly ordinary form that
nothing wraps anymore, so it sails through the browser. Clicking “Apply”
submits only the coupon, clicking “Place order” the whole order.
The id is derived from the component name, that is frm- plus the
name (frm-orderForm). If you need a specific value, set it via
$form->getElementPrototype()->id.
You don't have to think hard about which form to mark. The outer form must
always be detached, the inner one may stay ordinary. And since
marking the inner one detached too does no harm, the simplest
approach is to just mark both. The same holds for deeper nesting: however you
stack the forms, feel free to mark them all detached.
The same principle powers POST links
If that trick with an empty <form> and the
form attribute looks familiar, you are right. It is exactly what
the best practice POST links is
built on. Actions that change server state, like deleting, do not belong under a
GET link, so you put one empty form in the layout and hang the buttons
on it:
<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}">delete</button>
</td>
</tr>
</table>
You then protect the action against being triggered from another site with
the #[Requires] attribute (for signals the origin check is
automatic):
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} is really this manual technique wrapped into a
full Nette form. Once you write it by hand for a couple of buttons, the next
time you let it be generated for an entire form.
{form scope}:
render just a piece of a form
Alongside detached, the template gained a second tag,
{form scope}. It tells the {input} and
{label} elements inside it which form or container they belong to,
but renders no <form> tag itself. It comes in handy in two
situations.
First: you are rendering the fields of some container in a form. To avoid
repeating the full path like {input address-street} for each one,
{form scope} switches the context to the container and the names
inside are then written relatively:
<form n:name="signForm">
{form scope address}
{input street}
{input city}
{/form}
</form>
Second: over AJAX you redraw just a piece of an already rendered form, say a
single field in a snippet. You must not render a new <form>
tag here, since it is already on the page, but {input} still has to
know where it belongs:
{snippet captcha}
{form scope signForm}
{input captcha}
{/form}
{/snippet}
It probably reminds you of the older {formContext} and
{formContainer}. And those are exactly what scope
replaces. Each of them did one of those two things and people kept mixing them
up; a single keyword that adapts to the context is easier to remember. Both old
tags still work, by the way.
Conclusion
What HTML forbade, the form attribute brings back through the
back door. Nette Forms 3.3 turns it into a single word in your template,
detached, and the same principle that keeps POST links safe now
serves full-blown nested forms too. Just upgrade to Forms 3.3 and Latte
3.1. There is more in the release, like modernized CSRF
protection that checks the Sec-Fetch-Site header instead of
a token.
Further reading
- Latte 3.1: When a templating system truly understands HTML
- A Quarter Century of CSRF. Now the Browser Finally Handles It
- Latte as a Ghost in the Machine: a Template Compiler for Tracy
- Component Model 4.0: top-down and no more magic
- New Elements for Your Forms: Date, Time, Colors, and Floats
- Nette Assets: Finally unified API for everything from images to Vite
Sign in to submit a comment