(Bildnachweis: https://www.maicar.com/GML/Ajax1.html)
Vor kurzem habe ich auf Mastodon ein Gespräch darüber geführt, wie ich Htmx mit großem Erfolg verwendet habe, und jemand hat mich in meinen Erwähnungen dazu herausgefordert, und dass Htmx tatsächlich eine ziemlich starke Abhängigkeit darstellt, wenn man bedenkt, wofür ich es verwende. Sie haben mich mit diesem Beitrag und allem verlinkt.
Zuerst war ich etwas genervt. Ich dachte, dass es mir ziemlich gut gelingt, die Dinge leichtgewichtig zu halten, und Htmx hat mir gute Dienste geleistet, aber dann habe ich den Hut aufgesetzt, den ich die ganze Zeit über zu tragen versucht habe, wenn es darum geht, die Art und Weise, wie ich Webentwicklung mache, neu zu erfinden : Sind meine Annahmen richtig? Kann ich es besser machen?
Also habe ich meine gesamte Nutzung von htmx durch eine winzige, 100 Zeilen lange Vanillajs-Webkomponente ersetzt, die ich vollständig in diesen Beitrag einbeziehen werde:
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);
Sie verwenden es so:
<ajax-it> <form action="/some/url"> <input name=name> </form> </ajax-it>
Und das ist es! Alle in der Antwort enthaltenen Elemente mit einer ID werden ersetzt, wenn die Antwort zurückkommt. Es funktioniert für Elemente auch!
Das Element funktioniert im Wesentlichen auf zwei Arten:
Also, mit etwas HTML wie diesem:
<div id=extra-stuff></div> <div id=user-list></div> <ajax-it> <a href="/users/list#put-it-here"> Get users </a> </ajax-it>
und eine Serverantwort wie diese:
<ul> <li>user 1 <li>user 2 </ul>
Das Ergebnis ist:
<ul> <li>user 1 <li>user 2 </ul>Get users
Aber wenn Ihre Antwort gewesen wäre:
<ul> <li>user 1 <li>user 2 </ul>Hello, I'm out-of-band
Sie hätten am Ende Folgendes erhalten:
Hello, I'm out-of-band
<ul> <li>user 1 <li>user 2 </ul>Get users
...mit der id=extra-stuff außerhalb des Bandes ausgetauscht und dem
Um die Idempotenz aufrechtzuerhalten, verwende ich jedoch nicht die Hash-Version von Dingen und stelle nur sicher, dass alle meine Antwortelemente über angehängte IDs verfügen:
<ul id=user-list> <li>user 1 <li>user 2 </ul> <p id=extra-stuff>Hello, I'm out-of-band</p>
Wodurch das