web-componentcustom-element

Clicking button doesn't submit form when the button is filled into a web component slot


I expect the two "forms" in the snippet below to behave the same when a <button> is clicked. A click on the button should submit the <form>. What I observe, however, is that it doesn't submit the form when the button is filled into a <slot> in a web component custom element like below:

<my-form>
  <button name="btn" value="out">OUT</button>
</my-form>
<form>
  Normal form: <button name="btn" value="out">OUT</button>
</form>

<template id="templ-my-form">
  <form>
    Custom element: <slot></slot>
  </form>
</template>
customElements.define(
  "my-form",
  class extends HTMLElement {
    constructor() {
      super();
      const templ = document.getElementById("templ-my-form")
      const shadowRoot = this.attachShadow({ mode: "open" })
      shadowRoot.appendChild(templ.content.cloneNode(true))
      
      shadowRoot.querySelector("form").onsubmit = (e) => {
        e.preventDefault()
        console.log("SUBMITTED")
      }
    }
  }
)

document.querySelector('form').onsubmit = e => {
  e.preventDefault()
  console.log("SUBMITTED")
}

CodePen https://codepen.io/iamlazynic/pen/XJJEJWG where you can see it in action.

Could anyone explain why it doesn't work the same as without using templates and slots? Is there a way to make it work in the same way while using custom elements (web component)?


Solution

  • The shadowDOM boundary creates another context.
    "outside" buttons have no clue there is a <form>; and your <button> in lightDOM is NOT "inside" shadowDOM because slotted content is REFLECTED (not moved) to shadowDOM

    For a long read on slots and slotted see: ::slotted CSS selector for nested children in shadowDOM slot

    So you have 2 options:

    1. create your button inside shadowDOM

    2. Attached a click handler to your slotted button in lightDOM, and submit the FORM with JavaScript

    Quick example code:

    <my-form>
      <button>lightDOM button in default SLOT</button>
    </my-form>
    <template id="MY-FORM">
      <form>
        shadowDOM_form <slot><!--<button> REFLECTED here--></slot>
      </form>
    </template>
    <script>
      const createElement = (tag, props = {}) => Object.assign(document.createElement(tag), props)
      customElements.define( "my-form", class extends HTMLElement {
          constructor() {
            super()  // sets AND returns 'this' scope
              .attachShadow({ mode: "open" }) // sets AND returns this.shadowRoot
              .append(
                document.getElementById(this.nodeName).content.cloneNode(true),
                createElement("button", { // Option #1 create the button
                  textContent: "shadowDOM button",
                  onclick : (evt) => this.submitted(evt)
                }),
              )
            // Opition #2 Use the (slotted) button in lightDOM
            let lightDOMbutton = this.querySelector("button");
            lightDOMbutton.onclick = (evt) => this.submitted(evt);
          }
          submitted(evt) {
            let form = this.shadowRoot.querySelector("form");
            console.log("submitted form", evt.currentTarget.textContent)
            evt.preventDefault()
            // form.submit(); // submit with JavaScript
          }
        })
    </script>