Can you nest HTML forms?

Can you nest HTML forms?

According to this SO post, well... no. What if there was a way to do it anyway ?

Nesting form is not allowed

If you try to do this:

<form action="/main">
    <input type="text" name="main_field">
    <form action="/sub">
        <input type="text" name="sub_field">
        <input type="submit">
    </form>
    <input type="submit">
</form>

You will notice that the nested form is discarded when parsed by the browser because it's not allowed in the spec. Nested forms might seem dumb, but it can be useful sometimes, for example when nesting some kind of modal in a form with specific actions.

It turns out this was exactly my use case with pop-modal and I found a nice solution that I wanted to share with you.

A fake form to the rescue

Let's tweak our snippet a little bit

<div id="holder">
<form action="/main">
    <input type="text" name="main_field">
    <nested-form action="/sub">
        <input type="text" name="sub_field">
        <input type="submit">
    </nested-form>
    <input type="submit">
</form>
</div>

Here, I converted the form tag to a custom nested-form tag. This way, I can keep the form tag and declare any attribute I want. But obviously, this is not going to work, so we need to do a few things.

const holder = document.querySelector("#holder");
const nestedForm = holder.querySelector("nested-form");
const form = document.createElement("form");
nestedForm.getAttributeNames().forEach((attr) => {
    form.setAttribute(attr, nestedForm.getAttribute(attr));
});
form.append(...nestedForm.childNodes);
nestedForm.remove();
holder.append(form);

This will effectively "unnest" the forms, but it will also move the fields in the dom. In my case, since I'm working in a modal, that's good enough, but maybe you actually need these fields to stay in place.

A fake form with fake fields

Let's say you cannot move the form in the dom. Here is a slightly more complex solution. Thanks to the form attribute, we target a form with nested hidden inputs. Those hidden input get the value for the original inputs through a change listener. It's not a bulletproof solution, but this works well for simple forms.

const holder = document.querySelector("#holder2");
const nestedForm = holder.querySelector("nested-form");
const form = document.createElement("form");
document.body.append(form);
nestedForm.getAttributeNames().forEach((attr) => {
  form.setAttribute(attr, nestedForm.getAttribute(attr));
});
form.id = form.id|| "form-" + (+new Date());
nestedForm.querySelectorAll('[type="submit"]').forEach((btn) => {
  btn.setAttribute("form", form.id);
});
nestedForm.querySelectorAll("input,textarea,select").forEach((input) => {
  const hidden = document.createElement('input');
  hidden.type = "hidden";
  hidden.name = input.name;
  form.appendChild(hidden);
  // Sync values
  input.addEventListener('change', (ev) => {
    hidden.value = input.value;
  })
});

If you want to give this a try, here is a codepen