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)?
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:
create your button inside shadowDOM
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>