I replaced htmx with a simple web component

(Image credit: https://www.maicar.com/GML/Ajax1.html)

I recently had a conversation on Mastodon about how I was using htmx to much success, and someone rolled into my mentions challenging me on that, and how htmx is actually a pretty heavy dependency …


This content originally appeared on DEV Community and was authored by Kat Marchán

(Image credit: https://www.maicar.com/GML/Ajax1.html)

I recently had a conversation on Mastodon about how I was using htmx to much success, and someone rolled into my mentions challenging me on that, and how htmx is actually a pretty heavy dependency considering what I was using it for. They linked me to this post and everything.

At first, I was kind of annoyed. I thought I was doing a pretty good job of keeping things lightweight, and htmx had served me well, but then I put on the hat that I've been trying to wear this whole time when it comes to reinventing the way I do web dev: are my assumptions right? Can I do better?

So I went ahead and replace my entire usage of htmx with a tiny, 100-line, vanillajs web component, that I'm going to include in this post in its entirety:

export class AjaxIt extends HTMLElement {
  constructor() {
    super();
    this.addEventListener("submit", this.#handleSubmit);
    this.addEventListener("click", this.#handleClick);
  }

  #handleSubmit(e: SubmitEvent) {
    const form = e.target as HTMLFormElement;
    if (form.parentElement !== this) return;
    e.preventDefault();
    const beforeEv = new CustomEvent("ajax-it:beforeRequest", {
      bubbles: true,
      composed: true,
      cancelable: true,
    });
    form.dispatchEvent(beforeEv);
    if (beforeEv.defaultPrevented) {
      return;
    }
    const data = new FormData(form);
    form.dispatchEvent(new CustomEvent("ajax-it:beforeSend", { bubbles: true, composed: true }));
    const action = (e.submitter as HTMLButtonElement | null)?.formAction || form.action;
    (async () => {
      try {
        const res = await fetch(action, {
          method: form.method || "POST",
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
            "Ajax-It": "true",
          },
          body: new URLSearchParams(data as unknown as Record<string, string>),
        });
        if (!res.ok) {
          throw new Error("request failed");
        }
        form.dispatchEvent(new CustomEvent("ajax-it:afterRequest", { bubbles: true, composed: true }));
        const text = await res.text();
        this.#injectReplacements(text, new URL(res.url).hash);
      } catch {
        form.dispatchEvent(new CustomEvent("ajax-it:requestFailed", { bubbles: true, composed: true }));
      }
    })();
  }

  #handleClick(e: MouseEvent) {
    const anchor = e.target as HTMLAnchorElement;
    if (anchor.tagName !== "A" || anchor.parentElement !== this) return;
    e.preventDefault();
    anchor.dispatchEvent(new CustomEvent("ajax-it:beforeRequest", { bubbles: true, composed: true }));
    anchor.dispatchEvent(new CustomEvent("ajax-it:beforeSend", { bubbles: true, composed: true }));
    (async () => {
      try {
        const res = await fetch(anchor.href, {
          method: "GET",
          headers: {
            "Ajax-It": "true",
          },
        });
        if (!res.ok) {
          throw new Error("request failed");
        }
        anchor.dispatchEvent(new CustomEvent("ajax-it:afterRequest", { bubbles: true, composed: true }));
        const text = await res.text();
        this.#injectReplacements(text, new URL(res.url).hash);
      } catch {
        anchor.dispatchEvent(new CustomEvent("ajax-it:requestFailed", { bubbles: true, composed: true }));
      }
    })();
  }

  #injectReplacements(html: string, hash: string) {
    setTimeout(() => {
      const div = document.createElement("div");
      div.innerHTML = html;
      const mainTargetConsumed = !!hash && !!div.querySelector(
        hash,
      );
      const elements = [...div.querySelectorAll("[id]") ?? []];
      for (const element of elements.reverse()) {
        // If we have a parent that's already going to replace us, don't bother,
        // it will be dragged in when we replace the ancestor.
        const parentWithID = element.parentElement?.closest("[id]");
        if (parentWithID && document.getElementById(parentWithID.id)) {
          continue;
        }
        document.getElementById(element.id)?.replaceWith(element);
      }
      if (mainTargetConsumed) return;
      if (hash) {
        document
          .querySelector(hash)
          ?.replaceWith(...div.childNodes || []);
      }
    });
  }
}
customElements.define("ajax-it", AjaxIt);

You use it like this:

<ajax-it>
  <form action="/some/url">
    <input name=name>
  </form>
</ajax-it>

And that's it! Any elements with an id included in the response will be replaced when the response comes back. It works for <a> elements, too!

It's also fully progressively enhanced: as long as your action attribute points to a regular endpoint, things will behave as expected if JS isn't working or fails to load. All you have to look for on the server side is an Ajax-It: true header, so you can respond with minimal html instead of a full response.

Huge kudos and credit to htmz, which this is largely based on, except I needed to do it with AJAX instead of the iframe trick because I actually needed lifecycle events to do some of the offline trickery I'm doing.

Anyway cheers. Feel free to use the element in your own stuff! Consider it public domain :)


This content originally appeared on DEV Community and was authored by Kat Marchán


Print Share Comment Cite Upload Translate Updates
APA

Kat Marchán | Sciencx (2024-09-06T19:01:59+00:00) I replaced htmx with a simple web component. Retrieved from https://www.scien.cx/2024/09/06/i-replaced-htmx-with-a-simple-web-component/

MLA
" » I replaced htmx with a simple web component." Kat Marchán | Sciencx - Friday September 6, 2024, https://www.scien.cx/2024/09/06/i-replaced-htmx-with-a-simple-web-component/
HARVARD
Kat Marchán | Sciencx Friday September 6, 2024 » I replaced htmx with a simple web component., viewed ,<https://www.scien.cx/2024/09/06/i-replaced-htmx-with-a-simple-web-component/>
VANCOUVER
Kat Marchán | Sciencx - » I replaced htmx with a simple web component. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/09/06/i-replaced-htmx-with-a-simple-web-component/
CHICAGO
" » I replaced htmx with a simple web component." Kat Marchán | Sciencx - Accessed . https://www.scien.cx/2024/09/06/i-replaced-htmx-with-a-simple-web-component/
IEEE
" » I replaced htmx with a simple web component." Kat Marchán | Sciencx [Online]. Available: https://www.scien.cx/2024/09/06/i-replaced-htmx-with-a-simple-web-component/. [Accessed: ]
rf:citation
» I replaced htmx with a simple web component | Kat Marchán | Sciencx | https://www.scien.cx/2024/09/06/i-replaced-htmx-with-a-simple-web-component/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.