From 35e3b1331b3a1d4359c3b42f6edaa03792f0da10 Mon Sep 17 00:00:00 2001 From: rameel Date: Tue, 13 Oct 2026 21:43:42 +0500 Subject: [PATCH] script: move child nodes into a fragment instead of spreading them The innerMorph/outerMorph fallback and the outerSync fallback for HTMX 1.9.x and 2.x spread the response's child nodes into replaceChildren and replaceWith. Engines limit the number of call arguments: about 125,000 in Chromium, 500,000 in Firefox and 65,536 in Safari. A larger response made handleSwap throw a RangeError, and HTMX logged it and fell back to the default swap style: outerMorph inserted the response inside the target, and outerSync nested the new element inside the target, whose attributes had already been synchronized. Move the nodes into a DocumentFragment and pass it as a single argument. --- .../wwwroot/htmx-toolkit.js | 19 ++++++++++++++++--- .../wwwroot/htmx-toolkit.min.js | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.js b/src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.js index 32591ce..b65fe9d 100644 --- a/src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.js +++ b/src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.js @@ -6,6 +6,17 @@ document._r_htmx ||= ((document, htmx) => { // whether or not any element enables the extension with hx-ext. let api; + // Spreading child nodes into replaceChildren/replaceWith hits the engine's argument limit + // (65536 in Safari), so the nodes are moved into a fragment instead. + const extract_children = node => { + let fragment = new DocumentFragment(); + while (node.firstChild) { + fragment.append(node.firstChild); + } + + return fragment; + }; + if (htmx.defineExtension) { htmx.defineExtension("ramstack-swap", { init(internal_api) { @@ -33,9 +44,11 @@ document._r_htmx ||= ((document, htmx) => { warn(`Idiomorph is unavailable; falling back from ${swap_style} to ${morph_style}`); let nodes = [...fragment.childNodes]; + let replacement = extract_children(fragment); + morph_style === "innerHTML" - ? target.replaceChildren(...nodes) - : target.replaceWith(...nodes); + ? target.replaceChildren(replacement) + : target.replaceWith(replacement); return nodes; } @@ -53,7 +66,7 @@ document._r_htmx ||= ((document, htmx) => { target.setAttribute(attr.name, attr.value); } - target.replaceChildren(...source.childNodes); + target.replaceChildren(extract_children(source)); // Like HTMX 4.x, report the target itself: HTMX processes it with // its new children and re-initializes it if its attributes changed. diff --git a/src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.min.js b/src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.min.js index c436e52..73ea922 100644 --- a/src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.min.js +++ b/src/Ramstack.HtmxToolkit/wwwroot/htmx-toolkit.min.js @@ -1 +1 @@ -document._r_htmx||=((e,t)=>{const r=e=>console.warn(`ramstack.htmxtoolkit: ${e}`),a=parseInt(t.version);let n;t.defineExtension&&t.defineExtension("ramstack-swap",{init(e){n=e},isInlineSwap:e=>"outerMorph"===e||"outerSync"===e,handleSwap(e,t,a){if("textContent"===e)return t.textContent=a.textContent,[t];let n="innerMorph"===e?"innerHTML":"outerMorph"===e?"outerHTML":null;if(n){let o=globalThis.Idiomorph;if(o?.morph)return o.morph(t,a.children,{morphStyle:n});r(`Idiomorph is unavailable; falling back from ${e} to ${n}`);let l=[...a.childNodes];return"innerHTML"===n?t.replaceChildren(...l):t.replaceWith(...l),l}if("outerSync"===e){r("outerSync requires HTMX 4.x; falling back to attribute sync and innerHTML");let e=a.firstElementChild;if(e){for(let r of[...t.attributes])e.hasAttribute(r.name)||t.removeAttribute(r.name);for(let r of e.attributes)t.setAttribute(r.name,r.value);return t.replaceChildren(...e.childNodes),[t]}}}});const o=(t,r)=>{e.addEventListener(t,r)},l=e=>{let t=e.querySelector("meta[name='htmx-config']")?.dataset||{};return{headerName:t.antiforgeryHeaderName,formFieldName:t.antiforgeryFormFieldName,requestToken:t.antiforgeryRequestToken}};let i=l(e);const s=(e,t,r)=>{if(!/^get$/i.test(e)){const{headerName:e,formFieldName:a,requestToken:n}=i;n&&(r.has?.(a)||r[a]||(e?t[e]=n:r.set?r.set(a,n):r[a]=n))}},u=e=>{let t=(new DOMParser).parseFromString(e||"","text/html"),r=l(t);r.requestToken&&(i=r)};return o("htmx:afterOnLoad",e=>{let t=e.detail;t.boosted&&u(t.xhr.responseText)}),o("htmx:after:request",e=>{let t=e.detail.ctx;t.boosted&&u(t.text)}),o("htmx:configRequest",e=>{let t=e.detail;s(t.verb,t.headers,t.parameters)}),o("htmx:config:request",e=>{let t=e.detail.ctx.request;s(t.method,t.headers,t.body)}),o("htmx:beforeRequest",t=>{if(4>a){let r=t.target,{xhr:a,target:o,select:l,requestConfig:i}=t.detail;null!=i.headers["HX-Request-Type"]||(e=>{let t=!1;return n.getClosestMatch(e,e=>{let r=n.getAttributeValue(e,"hx-request")?.trim();if(r&&!/^(js|javascript):/.test(r))try{let{noHeaders:e}=JSON.parse("{"===r[0]?r:`{${r}}`);if(null!=e)return t=!!e,!0}catch{}}),t})(r)||(l||=n.getClosestAttributeValue(r,"hx-select"),a.setRequestHeader("HX-Request-Type",o===e.body||l&&"unset"!==l?"full":"partial"))}}),o("rs:event",e=>{let r=e.target;for(let{key:n,value:o}of e.detail.value||e.detail){let e=r;4>a?"[object Object]"===Object.prototype.toString.call(o)?2===a&&void 0!==o.target&&(r=e=o.target):o={value:o}:(o?.target&&(e=t.find(o.target)),"object"!=typeof o&&(o={value:o})),t.trigger(e,n,o)}}),!0})(document,htmx); \ No newline at end of file +document._r_htmx||=((e,t)=>{const r=e=>console.warn(`ramstack.htmxtoolkit: ${e}`),a=parseInt(t.version);let n;const o=e=>{let t=new DocumentFragment;for(;e.firstChild;)t.append(e.firstChild);return t};t.defineExtension&&t.defineExtension("ramstack-swap",{init(e){n=e},isInlineSwap:e=>"outerMorph"===e||"outerSync"===e,handleSwap(e,t,a){if("textContent"===e)return t.textContent=a.textContent,[t];let n="innerMorph"===e?"innerHTML":"outerMorph"===e?"outerHTML":null;if(n){let l=globalThis.Idiomorph;if(l?.morph)return l.morph(t,a.children,{morphStyle:n});r(`Idiomorph is unavailable; falling back from ${e} to ${n}`);let i=[...a.childNodes],s=o(a);return"innerHTML"===n?t.replaceChildren(s):t.replaceWith(s),i}if("outerSync"===e){r("outerSync requires HTMX 4.x; falling back to attribute sync and innerHTML");let e=a.firstElementChild;if(e){for(let r of[...t.attributes])e.hasAttribute(r.name)||t.removeAttribute(r.name);for(let r of e.attributes)t.setAttribute(r.name,r.value);return t.replaceChildren(o(e)),[t]}}}});const l=(t,r)=>{e.addEventListener(t,r)},i=e=>{let t=e.querySelector("meta[name='htmx-config']")?.dataset||{};return{headerName:t.antiforgeryHeaderName,formFieldName:t.antiforgeryFormFieldName,requestToken:t.antiforgeryRequestToken}};let s=i(e);const u=(e,t,r)=>{if(!/^get$/i.test(e)){const{headerName:e,formFieldName:a,requestToken:n}=s;n&&(r.has?.(a)||r[a]||(e?t[e]=n:r.set?r.set(a,n):r[a]=n))}},d=e=>{let t=(new DOMParser).parseFromString(e||"","text/html"),r=i(t);r.requestToken&&(s=r)};return l("htmx:afterOnLoad",e=>{let t=e.detail;t.boosted&&d(t.xhr.responseText)}),l("htmx:after:request",e=>{let t=e.detail.ctx;t.boosted&&d(t.text)}),l("htmx:configRequest",e=>{let t=e.detail;u(t.verb,t.headers,t.parameters)}),l("htmx:config:request",e=>{let t=e.detail.ctx.request;u(t.method,t.headers,t.body)}),l("htmx:beforeRequest",t=>{if(4>a){let r=t.target,{xhr:a,target:o,select:l,requestConfig:i}=t.detail;null!=i.headers["HX-Request-Type"]||(e=>{let t=!1;return n.getClosestMatch(e,e=>{let r=n.getAttributeValue(e,"hx-request")?.trim();if(r&&!/^(js|javascript):/.test(r))try{let{noHeaders:e}=JSON.parse("{"===r[0]?r:`{${r}}`);if(null!=e)return t=!!e,!0}catch{}}),t})(r)||(l||=n.getClosestAttributeValue(r,"hx-select"),a.setRequestHeader("HX-Request-Type",o===e.body||l&&"unset"!==l?"full":"partial"))}}),l("rs:event",e=>{let r=e.target;for(let{key:n,value:o}of e.detail.value||e.detail){let e=r;4>a?"[object Object]"===Object.prototype.toString.call(o)?2===a&&void 0!==o.target&&(r=e=o.target):o={value:o}:(o?.target&&(e=t.find(o.target)),"object"!=typeof o&&(o={value:o})),t.trigger(e,n,o)}}),!0})(document,htmx); \ No newline at end of file