There is an issue when using web components (e.g., Shoelace) with form elements. The problem is that web components, in general, take some time before they are fully registered/loaded. Shoelace registers 'submit' event listeners, which handle validations, but because it happens after web components are defined (customElements.whenDefined), HTMX registers its submit listener first, and when the user clicks the submit button, the form is sent to a server, even before shoelace validations are fired up. So, the order of registered 'submit' event listeners is the problem here.
This will completely ignore the required attribute on sl-input. You may argue that this can be easily handled by using hx-disable on the form and then call htmx.process manually after web components are loaded. Like this:
And this will work! ... until you start playing with how it behaves with a history cache.
A snapshot of the body is stored in local storage without hx-disable on the form element (the attribute is already removed by the script), and when this page is restored, it is processed by HTMX in a standard way, and the htmx 'submit' event is again registered first. It will run htmx.process on the form without hx-disabled when web components are defined, but this won't help now since htmx.process does not clean up already processed elements, so the order of listeners stays the same. You can check this in devtools console by executing the following command: getEventListeners(document.getElementsByTagName('form')[0]).submit
If the cleanUpElement function was a public API, it could work like this:
This has actually worked for me in every scenario so far because it re-registers all listeners, which means they are now registered after Shoelace's listeners.
Perhaps, instead of exposing a new API function, it would be better to extend the process function to take some options argument so a user can specify if he wants to clean up a given element first. Just an idea...
I think this is the best way to approach this issue without adding extra complexity to it. It is clearly stated in Shoelace's documentation:
Shoelace uses event listeners to intercept the form’s formdata and submit events. This allows it to inject data and trigger validation as necessary. If you’re also attaching an event listener to the form, you must attach it after Shoelace form controls are connected to the DOM, otherwise your logic will run before Shoelace has a chance to inject form data and validate form controls.
Maybe it could be solved differently, but anything that forces a programmer to think twice about handling such a simple case would be a potential shoot-in-the-foot scenario, especially when it works in normal flow but crashes when a user navigates to the page through a back button with a default history cache settings. This approach works with boost and does not require extra hx- attributes.
I think this is a 1:1 alternative with a current API:
<script type="text/javascript" type="module">
Promise.allSettled([
customElements.whenDefined("sl-input"),
customElements.whenDefined("sl-button")
]).then(() => {
console.log("web components loaded")
const form = htmx.find("form")
form.setAttribute("hx-disable", "true")
// now it works as a clean up
htmx.process(form)
form.removeAttribute("hx-disable")
// now it registers everything again
htmx.process(form)
})
</script>
This is a bit cumbersome; a single function call could solve it.
Any thoughts?
There is an issue when using web components (e.g., Shoelace) with form elements. The problem is that web components, in general, take some time before they are fully registered/loaded. Shoelace registers 'submit' event listeners, which handle validations, but because it happens after web components are defined (
customElements.whenDefined
), HTMX registers its submit listener first, and when the user clicks the submit button, the form is sent to a server, even before shoelace validations are fired up. So, the order of registered 'submit' event listeners is the problem here.Consider this example:
This will completely ignore the
required
attribute onsl-input
. You may argue that this can be easily handled by usinghx-disable
on the form and then callhtmx.process
manually after web components are loaded. Like this:And this will work! ... until you start playing with how it behaves with a history cache.
A snapshot of the
body
is stored in local storage withouthx-disable
on theform
element (the attribute is already removed by the script), and when this page is restored, it is processed by HTMX in a standard way, and the htmx 'submit' event is again registered first. It will runhtmx.process
on theform
withouthx-disabled
when web components are defined, but this won't help now sincehtmx.process
does not clean up already processed elements, so the order of listeners stays the same. You can check this in devtools console by executing the following command:getEventListeners(document.getElementsByTagName('form')[0]).submit
If the
cleanUpElement
function was a public API, it could work like this:This has actually worked for me in every scenario so far because it re-registers all listeners, which means they are now registered after Shoelace's listeners.
Perhaps, instead of exposing a new API function, it would be better to extend the
process
function to take someoptions
argument so a user can specify if he wants to clean up a given element first. Just an idea...I think this is the best way to approach this issue without adding extra complexity to it. It is clearly stated in Shoelace's documentation:
Maybe it could be solved differently, but anything that forces a programmer to think twice about handling such a simple case would be a potential shoot-in-the-foot scenario, especially when it works in normal flow but crashes when a user navigates to the page through a back button with a default history cache settings. This approach works with
boost
and does not require extrahx-
attributes.I think this is a 1:1 alternative with a current API:
This is a bit cumbersome; a single function call could solve it. Any thoughts?