(이미지 출처: https://www.maicar.com/GML/Ajax1.html)
최근 Mastodon에서 내가 htmx를 어떻게 사용하여 큰 성공을 거두었는지에 대한 대화를 나눴는데, 누군가가 이에 대해 나에게 도전하는 내용을 언급했고, 내가 사용하는 목적을 고려할 때 htmx가 실제로 얼마나 큰 의존성을 가지고 있는지에 대해 이야기했습니다. 그들은 저를 이 게시물과 모든 것에 연결해 주었습니다.
처음에는 좀 짜증이 났어요. 나는 일을 가볍게 유지하는 일을 꽤 잘하고 있다고 생각했고 htmx가 나에게 많은 도움을 주었지만 웹 개발 방식을 재창조하는 데 있어서는 지금까지 계속 착용하려고 했던 모자를 썼습니다. : 내 가정이 맞나요? 더 잘할 수 있을까요?
그래서 저는 htmx의 전체 사용을 작은 100줄 바닐라 js 웹 구성 요소로 대체했습니다. 이 구성 요소 전체를 이 게시물에 포함하겠습니다.
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가 있는 모든 요소는 응답이 돌아올 때 대체됩니다. 요소도 있습니다!
요소는 두 가지 주요 방식으로 작동합니다.
그러므로 다음과 같은 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가 대역 외에서 교체되고
그러나 나는 멱등성을 유지하기 위해 해시 버전을 사용하지 않고 모든 응답 요소에 ID가 첨부되어 있는지 확인합니다.
<ul id=user-list> <li>user 1 <li>user 2 </ul> <p id=extra-stuff>Hello, I'm out-of-band</p>