A form inside a form? HTML says no, Nette 3.3 says yes

3 hours ago by David Grudl  

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.

David Grudl Programmer, blogger, and AI evangelist who created the Nette Framework powering hundreds of thousands of websites. He explores artificial intelligence on Uměligence and web development on phpFashion. Weekly, he hosts Tech Guys and teaches people to master ChatGPT and other AI tools. He's passionate about transformative technologies and excels at making them accessible to everyone.