(Crédit image : https://www.maicar.com/GML/Ajax1.html)
J'ai récemment eu une conversation sur Mastodon sur la façon dont j'utilisais htmx avec beaucoup de succès, et quelqu'un est entré dans mes mentions en me mettant au défi à ce sujet, et sur le fait que htmx est en fait une dépendance assez lourde compte tenu de l'usage que je l'utilisais. Ils m'ont lié à ce post et tout.
Au début, j'étais plutôt ennuyé. Je pensais que je faisais du bon travail en gardant les choses légères, et htmx m'avait bien servi, mais j'ai ensuite mis le chapeau que j'essayais de porter tout ce temps lorsqu'il s'agissait de réinventer ma façon de développer du Web. : mes hypothèses sont-elles correctes ? Puis-je faire mieux ?
J'ai donc décidé de remplacer toute mon utilisation de htmx par un petit composant Web vanillajs de 100 lignes, que je vais inclure dans cet article dans son intégralité :
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);
Vous l'utilisez comme ceci :
<ajax-it> <form action="/some/url"> <input name=name> </form> </ajax-it>
Et c'est tout ! Tous les éléments avec un identifiant inclus dans la réponse seront remplacés lorsque la réponse reviendra. Cela fonctionne pour des éléments aussi !
L'élément fonctionne de deux manières principales :
Donc, avec du HTML comme celui-ci :
<div id=extra-stuff></div> <div id=user-list></div> <ajax-it> <a href="/users/list#put-it-here"> Get users </a> </ajax-it>
et une réponse du serveur comme celle-ci :
<ul> <li>user 1 <li>user 2 </ul>
Vous obtiendrez :
<ul> <li>user 1 <li>user 2 </ul>Get users
Mais si votre réponse avait été :
<ul> <li>user 1 <li>user 2 </ul>Hello, I'm out-of-band
vous auriez fini avec :
Hello, I'm out-of-band
<ul> <li>user 1 <li>user 2 </ul>Get users
...avec l'id=extra-stuff échangé hors bande et le
Pour maintenir l'idempotence, cependant, je n'ai pas tendance à utiliser la version hachée des choses, et je m'assure simplement que tous mes éléments de réponse ont des identifiants joints :
<ul id=user-list> <li>user 1 <li>user 2 </ul> <p id=extra-stuff>Hello, I'm out-of-band</p>
Ce qui maintiendrait le