ホームページ  >  記事  >  ウェブフロントエンド  >  htmx を単純な Web コンポーネントに置き換えました

htmx を単純な Web コンポーネントに置き換えました

WBOY
WBOYオリジナル
2024-09-07 06:37:16536ブラウズ

I replaced htmx with a simple web component

(画像クレジット: https://www.maicar.com/GML/Ajax1.html)

私は最近、htmx を使用して大きな成功を収めていることについてマストドンで会話しました。そして、誰かが私のメンションに転がり込んできて、それについて私に挑戦し、htmx が実際に私が使用していた目的を考えるとかなり重い依存関係であることについて言及しました。彼らは私をこの投稿やあらゆるものにリンクしてくれました。

最初はちょっとイライラしました。軽量化に関してはかなりうまくやっていると思っていましたし、htmx も役に立ちました。しかし、Web 開発のやり方を再発明するということになると、これまでずっとかぶろうとしてきた帽子をかぶることになりました。 :私の推測は正しいでしょうか?もっと上手くできるでしょうか?

そこで、htmx の使用全体を、100 行の小さな vanillajs Web コンポーネントに置き換えることにしました。この投稿には、その全体を含めることにします。

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

次のように使用します:

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

それで終わりです!応答に含まれる ID を持つ要素は、応答が返されたときに置き換えられます。それは に対して機能します。要素も!

この要素は主に 2 つの方法で機能します:

  1. アクションまたは href にハッシュが含まれている場合、そのハッシュに一致する ID を持つ要素または現在のページ上の要素が、応答全体のコンテンツに置き換えられます。
  2. 返された HTML にそれ自体に ID を持つ要素が含まれており、それらの ID が現在のドキュメント内で一致する場合、それらの要素は最初に置換されます (そして、上記の「応答全体」の置換から除外されます)。これは基本的に「帯域外」スワップ (別名 hx-swap-oob) を行う方法です。

それでは、次のような HTML を使用します。

<div id=extra-stuff></div>
<div id=user-list></div>

<ajax-it>
  <a href="/users/list#put-it-here">
    Get users
  </a>
</ajax-it>

そして次のようなサーバー応答:

<ul>
  <li>user 1
  <li>user 2
</ul>

最終的には次のようになります:

<ul> <li>user 1 <li>user 2 </ul>
Get users

しかし、あなたの反応が次のようなものだったとします。

<ul>
  <li>user 1
  <li>user 2
</ul>

Hello, I'm out-of-band

最終的には次のようになります:

Hello, I'm out-of-band

<ul> <li>user 1 <li>user 2 </ul> Get users

...id=extra-stuff が帯域外で交換され、

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。