bigskysoftware / bigskysoftware/htmx

Make `cleanUpElement` public or make `htmx.process` configurable to always clean up a given element and its children

Open
#2,386 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
49.4k
Forks
1.7k
Avg merge
3d 22h
Merged PRs (30d)
30

Description

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:

```html



Submit


Promise.allSettled([
customElements.whenDefined("sl-input"),
customElements.whenDefined("sl-button")
]).then(() => {
console.log("web components loaded")
})

```

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:

```html



Submit


Promise.allSettled([
customElements.whenDefined("sl-input"),
customElements.whenDefined("sl-button")
]).then(() => {
console.log("web components loaded")
const form = htmx.find("form")
form.removeAttribute("hx-disable")
htmx.process(form)
})

```

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:

```html



Submit


Promise.allSettled([
customElements.whenDefined("sl-input"),
customElements.whenDefined("sl-button")
]).then(() => {
console.log("web components loaded")
const form = htmx.find("form")
htmx.cleanUpElement(form)
htmx.process(form)
})

```

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:

```html

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)
})

```

This is a bit cumbersome; a single function call could solve it.
Any thoughts?

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.