diff --git a/src/htmx.js b/src/htmx.js index 024baa612..ec854153e 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -398,7 +398,6 @@ var htmx = (() => { htmxProp.initialized = true; htmxProp.eventHandler = this.__createHtmxEventHandler(elt); this.__initializeTriggers(elt); - this.__initializeAbortListener(elt) this.__trigger(elt, "htmx:after:init", {}, true) } } @@ -580,6 +579,7 @@ var htmx = (() => { let elt = ctx.sourceElement let syncStrategy = this.__determineSyncStrategy(elt); let requestQueue = this.__getRequestQueue(elt); + this.__initializeAbortListener(elt); if (!requestQueue.issue(ctx, syncStrategy)) return @@ -2000,12 +2000,15 @@ var htmx = (() => { } __initializeAbortListener(elt) { + let htmxProp = this.__htmxProp(elt); + if (htmxProp.abortInitialized) return; + htmxProp.abortInitialized = true; let handler = () => { let requestQueue = this.__getRequestQueue(elt); requestQueue.abort(); }; elt.addEventListener("htmx:abort", handler); - elt._htmx.listeners.push({fromElt: elt, eventName: "htmx:abort", handler}); + htmxProp.listeners.push({fromElt: elt, eventName: "htmx:abort", handler}); } __morph(oldNode, fragment, innerHTML) { diff --git a/test/lib/fetch-mock.js b/test/lib/fetch-mock.js index b2e74ed22..145c0021e 100644 --- a/test/lib/fetch-mock.js +++ b/test/lib/fetch-mock.js @@ -207,7 +207,8 @@ class FetchMock { options.method = options.method.toUpperCase() const response = this.findResponse(options.method, url); - // Create an AbortController for this request + // Use the caller's signal if provided, otherwise create our own + const callerSignal = options.signal; const controller = new AbortController(); // Create a tracking object for this request @@ -215,13 +216,24 @@ class FetchMock { // Create a promise to track this request const requestPromise = new Promise((resolve, reject) => { - // Check if already aborted + // Check if already aborted (caller's signal) + if (callerSignal?.aborted) { + reject(new DOMException('The operation was aborted', 'AbortError')); + return; + } + + // Check if already aborted (our controller) if (controller.signal.aborted) { reject(new DOMException('The operation was aborted', 'AbortError')); return; } - // Listen for abort + // Listen for abort from caller's signal + callerSignal?.addEventListener('abort', () => { + reject(new DOMException('The operation was aborted', 'AbortError')); + }); + + // Listen for abort from our controller controller.signal.addEventListener('abort', () => { reject(new DOMException('The operation was aborted', 'AbortError')); }); @@ -230,12 +242,12 @@ class FetchMock { Promise.resolve(response) .then(result => { if (typeof result === 'string') result = new MockResponse(result); - if (!controller.signal.aborted) { + if (!callerSignal?.aborted && !controller.signal.aborted) { resolve(result); } }) .catch(error => { - if (!controller.signal.aborted) { + if (!callerSignal?.aborted && !controller.signal.aborted) { reject(error); } }); diff --git a/test/tests/unit/ajax.js b/test/tests/unit/ajax.js index cb3ab30c7..0e83aa192 100644 --- a/test/tests/unit/ajax.js +++ b/test/tests/unit/ajax.js @@ -309,4 +309,32 @@ describe('ajax() unit Tests', function() { assert.include(div.innerHTML, '
old
'); assert.include(div.innerHTML, 'new'); }); + + it('ajax request can be aborted via htmx:abort event', async function() { + const seq = mockSequentialResponses('GET', '/test', 'should not appear'); + const div = createProcessedHTML(''); + + let errorFired = false; + div.addEventListener('htmx:error', () => { + errorFired = true; + }); + + const promise = htmx.ajax('GET', '/test', { + source: '#source', + target: '#source', + swap: 'innerHTML' + }); + + // Wait for request to be issued, then abort before releasing response + await htmx.timeout(1); + htmx.trigger('#source', 'htmx:abort'); + + // Release the response (should be ignored since aborted) + seq.next(); + await promise; + + // Content should not have been swapped + assert.equal(div.innerHTML, ''); + assert.isTrue(errorFired); + }); });