mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-27 04:50:43 +00:00
Fix: Make the SSE extension work properly and adhere to tests (#2025)
* Process sse-swap properly when swapped in Previously `sse-swap` were only processed when `sse-connect` was on the root of what was being swapped in. This is now fixed so that the closest sseEventSource is found and used as the event source * Pass for code readabiltiy remove the nested if statements as there is no difference in handling between sseURL and LegacySSEUrl. * Fix indenting * Initial attempt at sse extension tests * Make existing tests pass * Add test case for #916 * backport test for #916 to hx-sse for completeness * add a distinct test for sse-swap * more tests, fix tests + more tests differentiating the implementation of sse-swap and hx-trigger * fix for "is closed after removal with no close and activity" * Cleanup listener if element is gone * Revert "Cleanup listener if element is gone" This reverts commit bbe3715276302656d1a422fbc4778842776e35f6. * Clean up listeners if element gone, fix indenting * Incorportate feedback from first review + seperate eventSource creation logic and event registering logic + manually create event handling, still confused by how hx-trigger works * `createEventSourceOnElement` now looks for event sources in children * explicitly handle legacy sse handling instead of having extra selectors in `querySelectorOnThisOrChildren` + a few readability changes + add regression check to make sure that sseEventSource is only created on elements with sse-connect or equivalent + add test to make sure that sse-connect in the child of a swapped element is handled * Allow multiple listeners in eventsource mock * Backport eventsource mocks to legacy tests --------- Co-authored-by: 1cg <469183+1cg@users.noreply.github.com>
This commit is contained in:
parent
9052a4d520
commit
11cef8ee83
211
src/ext/sse.js
211
src/ext/sse.js
@ -5,7 +5,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
*/
|
||||
|
||||
(function(){
|
||||
(function() {
|
||||
|
||||
/** @type {import("../htmx").HtmxInternalApi} */
|
||||
var api;
|
||||
@ -39,17 +39,19 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
switch (name) {
|
||||
|
||||
// Try to remove remove an EventSource when elements are removed
|
||||
case "htmx:beforeCleanupElement":
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
if (internalData.sseEventSource) {
|
||||
internalData.sseEventSource.close();
|
||||
}
|
||||
return;
|
||||
case "htmx:beforeCleanupElement":
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
// Try to remove remove an EventSource when elements are removed
|
||||
if (internalData.sseEventSource) {
|
||||
internalData.sseEventSource.close();
|
||||
}
|
||||
|
||||
// Try to create EventSources when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
createEventSourceOnElement(evt.target);
|
||||
return;
|
||||
|
||||
// Try to create EventSources when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
createEventSourceOnElement(evt.target);
|
||||
registerSSE(evt.target);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -66,8 +68,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
* @param {string} url
|
||||
* @returns EventSource
|
||||
*/
|
||||
function createEventSource(url) {
|
||||
return new EventSource(url, {withCredentials:true});
|
||||
function createEventSource(url) {
|
||||
return new EventSource(url, { withCredentials: true });
|
||||
}
|
||||
|
||||
function splitOnWhitespace(trigger) {
|
||||
@ -90,7 +92,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
function getLegacySSESwaps(elt) {
|
||||
var legacySSEValue = api.getAttributeValue(elt, "hx-sse");
|
||||
var returnArr = [];
|
||||
if (legacySSEValue) {
|
||||
if (legacySSEValue != null) {
|
||||
var values = splitOnWhitespace(legacySSEValue);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i].split(/:(.+)/);
|
||||
@ -103,63 +105,24 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
}
|
||||
|
||||
/**
|
||||
* createEventSourceOnElement creates a new EventSource connection on the provided element.
|
||||
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
||||
* is created and stored in the element's internalData.
|
||||
* registerSSE looks for attributes that can contain sse events, right
|
||||
* now hx-trigger and sse-swap and adds listeners based on these attributes too
|
||||
* the closest event source
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} retryCount
|
||||
* @returns {EventSource | null}
|
||||
*/
|
||||
function createEventSourceOnElement(elt, retryCount) {
|
||||
|
||||
if (elt == null) {
|
||||
return null;
|
||||
function registerSSE(elt) {
|
||||
// Find closest existing event source
|
||||
var sourceElement = api.getClosestMatch(elt, hasEventSource);
|
||||
if (sourceElement == null) {
|
||||
// api.triggerErrorEvent(elt, "htmx:noSSESourceError")
|
||||
return null; // no eventsource in parentage, orphaned element
|
||||
}
|
||||
|
||||
var internalData = api.getInternalData(elt);
|
||||
// Set internalData and source
|
||||
var internalData = api.getInternalData(sourceElement);
|
||||
var source = internalData.sseEventSource;
|
||||
|
||||
// get URL from element's attribute
|
||||
var sseURL = api.getAttributeValue(elt, "sse-connect");
|
||||
|
||||
|
||||
if (sseURL == undefined) {
|
||||
var legacyURL = getLegacySSEURL(elt)
|
||||
if (legacyURL) {
|
||||
sseURL = legacyURL;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Connect to the EventSource
|
||||
var source = htmx.createEventSource(sseURL);
|
||||
internalData.sseEventSource = source;
|
||||
|
||||
// Create event handlers
|
||||
source.onerror = function (err) {
|
||||
|
||||
// Log an error event
|
||||
api.triggerErrorEvent(elt, "htmx:sseError", {error:err, source:source});
|
||||
|
||||
// If parent no longer exists in the document, then clean up this EventSource
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, try to reconnect the EventSource
|
||||
if (source.readyState === EventSource.CLOSED) {
|
||||
retryCount = retryCount || 0;
|
||||
var timeout = Math.random() * (2 ^ retryCount) * 500;
|
||||
window.setTimeout(function() {
|
||||
createEventSourceOnElement(elt, Math.min(7, retryCount+1));
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
source.onopen = function (evt) {
|
||||
api.triggerEvent(elt, "htmx:sseOpen", {source: source});
|
||||
}
|
||||
|
||||
// Add message handlers for every `sse-swap` attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-swap").forEach(function(child) {
|
||||
|
||||
@ -170,23 +133,27 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
var sseEventNames = getLegacySSESwaps(child);
|
||||
}
|
||||
|
||||
for (var i = 0 ; i < sseEventNames.length ; i++) {
|
||||
for (var i = 0; i < sseEventNames.length; i++) {
|
||||
var sseEventName = sseEventNames[i].trim();
|
||||
var listener = function(event) {
|
||||
|
||||
// If the parent is missing then close SSE and remove listener
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
// If the source is missing then close SSE
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the body no longer contains the element, remove the listener
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
|
||||
// swap the response into the DOM and trigger a notification
|
||||
swap(child, event.data);
|
||||
api.triggerEvent(elt, "htmx:sseMessage", event);
|
||||
};
|
||||
|
||||
// Register the new listener
|
||||
api.getInternalData(elt).sseEventListener = listener;
|
||||
api.getInternalData(child).sseEventListener = listener;
|
||||
source.addEventListener(sseEventName, listener);
|
||||
}
|
||||
});
|
||||
@ -203,24 +170,86 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
if (sseEventName.slice(0, 4) != "sse:") {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove the sse: prefix from here on out
|
||||
sseEventName = sseEventName.substr(4);
|
||||
|
||||
var listener = function(event) {
|
||||
|
||||
// If parent is missing, then close SSE and remove listener
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
return;
|
||||
var listener = function() {
|
||||
if (maybeCloseSSESource(sourceElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger events to be handled by the rest of htmx
|
||||
htmx.trigger(child, sseEventName, event);
|
||||
htmx.trigger(child, "htmx:sseMessage", event);
|
||||
if (!api.bodyContains(child)) {
|
||||
source.removeEventListener(sseEventName, listener);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* createEventSourceOnElement creates a new EventSource connection on the provided element.
|
||||
* If a usable EventSource already exists, then it is returned. If not, then a new EventSource
|
||||
* is created and stored in the element's internalData.
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number} retryCount
|
||||
* @returns {EventSource | null}
|
||||
*/
|
||||
function createEventSourceOnElement(elt, retryCount) {
|
||||
|
||||
if (elt == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// handle extension source creation attribute
|
||||
queryAttributeOnThisOrChildren(elt, "sse-connect").forEach(function(child) {
|
||||
var sseURL = api.getAttributeValue(child, "sse-connect");
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Register the new listener
|
||||
api.getInternalData(elt).sseEventListener = listener;
|
||||
source.addEventListener(sseEventName.slice(4), listener);
|
||||
createEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
// handle legacy sse, remove for HTMX2
|
||||
queryAttributeOnThisOrChildren(elt, "hx-sse").forEach(function(child) {
|
||||
var sseURL = getLegacySSEURL(child);
|
||||
if (sseURL == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
createEventSource(child, sseURL, retryCount);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function createEventSource(elt, url, retryCount) {
|
||||
var source = htmx.createEventSource(url);
|
||||
|
||||
source.onerror = function(err) {
|
||||
|
||||
// Log an error event
|
||||
api.triggerErrorEvent(elt, "htmx:sseError", { error: err, source: source });
|
||||
|
||||
// If parent no longer exists in the document, then clean up this EventSource
|
||||
if (maybeCloseSSESource(elt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, try to reconnect the EventSource
|
||||
if (source.readyState === EventSource.CLOSED) {
|
||||
retryCount = retryCount || 0;
|
||||
var timeout = Math.random() * (2 ^ retryCount) * 500;
|
||||
window.setTimeout(function() {
|
||||
createEventSourceOnElement(elt, Math.min(7, retryCount + 1));
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
|
||||
source.onopen = function(evt) {
|
||||
api.triggerEvent(elt, "htmx:sseOpen", { source: source });
|
||||
}
|
||||
|
||||
api.getInternalData(elt).sseEventSource = source;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -253,12 +282,12 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
var result = [];
|
||||
|
||||
// If the parent element also contains the requested attribute, then add it to the results too.
|
||||
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-sse")) {
|
||||
if (api.hasAttribute(elt, attributeName)) {
|
||||
result.push(elt);
|
||||
}
|
||||
|
||||
// Search all child nodes that match the requested attribute
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [hx-sse], [data-hx-sse]").forEach(function(node) {
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "]").forEach(function(node) {
|
||||
result.push(node);
|
||||
});
|
||||
|
||||
@ -281,7 +310,7 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
|
||||
api.selectAndSwap(swapSpec.swapStyle, target, elt, content, settleInfo);
|
||||
|
||||
settleInfo.elts.forEach(function (elt) {
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.add(htmx.config.settlingClass);
|
||||
}
|
||||
@ -306,11 +335,11 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
function doSettle(settleInfo) {
|
||||
|
||||
return function() {
|
||||
settleInfo.tasks.forEach(function (task) {
|
||||
settleInfo.tasks.forEach(function(task) {
|
||||
task.call();
|
||||
});
|
||||
|
||||
settleInfo.elts.forEach(function (elt) {
|
||||
settleInfo.elts.forEach(function(elt) {
|
||||
if (elt.classList) {
|
||||
elt.classList.remove(htmx.config.settlingClass);
|
||||
}
|
||||
@ -319,4 +348,8 @@ This extension adds support for Server Sent Events to htmx. See /www/extensions
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
function hasEventSource(node) {
|
||||
return api.getInternalData(node).sseEventSource != null;
|
||||
}
|
||||
|
||||
})();
|
||||
|
@ -4,43 +4,53 @@ describe("hx-sse attribute", function() {
|
||||
var listeners = {};
|
||||
var wasClosed = false;
|
||||
var mockEventSource = {
|
||||
removeEventListener: function(name) {
|
||||
delete listeners[name];
|
||||
removeEventListener: function(name, l) {
|
||||
listeners[name] = listeners[name].filter(function(elt, idx, arr) {
|
||||
if (arr[idx] === l) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
},
|
||||
addEventListener: function (message, l) {
|
||||
listeners[message] = l;
|
||||
addEventListener: function(message, l) {
|
||||
if (listeners == undefined) {
|
||||
listeners[message] = [];
|
||||
}
|
||||
listeners[message].push(l)
|
||||
},
|
||||
sendEvent: function (eventName, data) {
|
||||
var listener = listeners[eventName];
|
||||
if (listener) {
|
||||
var event = htmx._("makeEvent")(eventName);
|
||||
event.data = data;
|
||||
listener(event);
|
||||
sendEvent: function(eventName, data) {
|
||||
var listeners = listeners[eventName];
|
||||
if (listeners) {
|
||||
listeners.forEach(function(listener) {
|
||||
var event = htmx._("makeEvent")(eventName);
|
||||
event.data = data;
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
close: function () {
|
||||
close: function() {
|
||||
wasClosed = true;
|
||||
},
|
||||
wasClosed: function () {
|
||||
wasClosed: function() {
|
||||
return wasClosed;
|
||||
}
|
||||
};
|
||||
return mockEventSource;
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
beforeEach(function() {
|
||||
this.server = makeServer();
|
||||
var eventSource = mockEventSource();
|
||||
this.eventSource = eventSource;
|
||||
clearWorkArea();
|
||||
htmx.createEventSource = function(){ return eventSource };
|
||||
htmx.createEventSource = function() { return eventSource };
|
||||
});
|
||||
afterEach(function () {
|
||||
afterEach(function() {
|
||||
this.server.restore();
|
||||
clearWorkArea();
|
||||
});
|
||||
|
||||
it('handles basic sse triggering', function () {
|
||||
it('handles basic sse triggering', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
this.server.respondWith("GET", "/d2", "div2 updated");
|
||||
@ -61,7 +71,7 @@ describe("hx-sse attribute", function() {
|
||||
byId("d2").innerHTML.should.equal("div2 updated");
|
||||
})
|
||||
|
||||
it('does not trigger events that arent named', function () {
|
||||
it('does not trigger events that arent named', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
|
||||
@ -82,7 +92,7 @@ describe("hx-sse attribute", function() {
|
||||
byId("d1").innerHTML.should.equal("div1 updated");
|
||||
})
|
||||
|
||||
it('does not trigger events not on descendents', function () {
|
||||
it('does not trigger events not on descendents', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
|
||||
@ -102,7 +112,7 @@ describe("hx-sse attribute", function() {
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
})
|
||||
|
||||
it('is closed after removal', function () {
|
||||
it('is closed after removal', function() {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-sse="connect:/foo">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
@ -112,7 +122,7 @@ describe("hx-sse attribute", function() {
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('is closed after removal with no close and activity', function () {
|
||||
it('is closed after removal with no close and activity', function() {
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-sse="connect:/foo">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
@ -121,7 +131,7 @@ describe("hx-sse attribute", function() {
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('swaps content properly on SSE swap', function () {
|
||||
it('swaps content properly on SSE swap', function() {
|
||||
var div = make('<div hx-sse="connect:/event_stream">\n' +
|
||||
' <div id="d1" hx-sse="swap:e1"></div>\n' +
|
||||
' <div id="d2" hx-sse="swap:e2"></div>\n' +
|
||||
@ -136,5 +146,15 @@ describe("hx-sse attribute", function() {
|
||||
byId("d2").innerText.should.equal("Event 2")
|
||||
})
|
||||
|
||||
it('swaps swapped in content', function() {
|
||||
var div = make('<div hx-sse="connect:/event_stream">\n' +
|
||||
'<div id="d1" hx-sse="swap:e1" hx-swap="outerHTML"></div>\n' +
|
||||
'</div>\n'
|
||||
)
|
||||
|
||||
this.eventSource.sendEvent("e1", '<div id="d2" hx-sse="swap:e2"></div>')
|
||||
this.eventSource.sendEvent("e2", 'Event 2')
|
||||
byId("d2").innerText.should.equal("Event 2")
|
||||
})
|
||||
});
|
||||
|
||||
|
232
test/ext/sse.js
Normal file
232
test/ext/sse.js
Normal file
@ -0,0 +1,232 @@
|
||||
const { assert } = require("chai");
|
||||
|
||||
describe("sse extension", function() {
|
||||
|
||||
function mockEventSource() {
|
||||
var listeners = {};
|
||||
var wasClosed = false;
|
||||
var url;
|
||||
var mockEventSource = {
|
||||
removeEventListener: function(name, l) {
|
||||
listeners[name] = listeners[name].filter(function(elt, idx, arr) {
|
||||
if (arr[idx] === l) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
},
|
||||
addEventListener: function(message, l) {
|
||||
if (listeners == undefined) {
|
||||
listeners[message] = [];
|
||||
}
|
||||
listeners[message].push(l)
|
||||
},
|
||||
sendEvent: function(eventName, data) {
|
||||
var listeners = listeners[eventName];
|
||||
if (listeners) {
|
||||
listeners.forEach(function(listener) {
|
||||
var event = htmx._("makeEvent")(eventName);
|
||||
event.data = data;
|
||||
listener(event);
|
||||
}
|
||||
}
|
||||
},
|
||||
close: function() {
|
||||
wasClosed = true;
|
||||
},
|
||||
wasClosed: function() {
|
||||
return wasClosed;
|
||||
},
|
||||
connect: function(url) {
|
||||
this.url = url
|
||||
}
|
||||
};
|
||||
return mockEventSource;
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
this.server = makeServer();
|
||||
var eventSource = mockEventSource();
|
||||
this.eventSource = eventSource;
|
||||
clearWorkArea();
|
||||
htmx.createEventSource = function(url) {
|
||||
eventSource.connect(url);
|
||||
return eventSource;
|
||||
};
|
||||
});
|
||||
afterEach(function() {
|
||||
this.server.restore();
|
||||
clearWorkArea();
|
||||
});
|
||||
|
||||
it('handles basic sse triggering', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
this.server.respondWith("GET", "/d2", "div2 updated");
|
||||
|
||||
var div = make('<div hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
'<div id="d2" hx-trigger="sse:e2" hx-get="/d2">div2</div>' +
|
||||
'</div>');
|
||||
|
||||
this.eventSource.sendEvent("e1");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1 updated");
|
||||
byId("d2").innerHTML.should.equal("div2");
|
||||
|
||||
this.eventSource.sendEvent("e2");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1 updated");
|
||||
byId("d2").innerHTML.should.equal("div2 updated");
|
||||
})
|
||||
|
||||
it('does not trigger events that arent named', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
|
||||
var div = make('<div hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
|
||||
this.eventSource.sendEvent("foo");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
|
||||
this.eventSource.sendEvent("e2");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
|
||||
this.eventSource.sendEvent("e1");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1 updated");
|
||||
})
|
||||
|
||||
it('does not trigger events not on descendents', function() {
|
||||
|
||||
this.server.respondWith("GET", "/d1", "div1 updated");
|
||||
|
||||
var div = make('<div hx-ext="sse" sse-connect="/foo"></div>' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>');
|
||||
|
||||
this.eventSource.sendEvent("foo");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
|
||||
this.eventSource.sendEvent("e2");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
|
||||
this.eventSource.sendEvent("e1");
|
||||
this.server.respond();
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
})
|
||||
|
||||
it('is closed after removal, hx-trigger', function() {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
div.click();
|
||||
this.server.respond();
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('is closed after removal, hx-swap', function() {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" hx-swap="e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
div.click();
|
||||
this.server.respond();
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('is closed after removal with no close and activity, hx-trigger', function() {
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" hx-trigger="sse:e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
div.parentElement.removeChild(div);
|
||||
this.eventSource.sendEvent("e1")
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
// sse and hx-trigger handlers are distinct
|
||||
it('is closed after removal with no close and activity, sse-swap', function() {
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="sse" sse-connect="/foo">' +
|
||||
'<div id="d1" sse-swap="e1" hx-get="/d1">div1</div>' +
|
||||
'</div>');
|
||||
div.parentElement.removeChild(div);
|
||||
this.eventSource.sendEvent("e1")
|
||||
this.eventSource.wasClosed().should.equal(true)
|
||||
})
|
||||
|
||||
it('swaps content properly on SSE swap', function() {
|
||||
var div = make('<div hx-ext="sse" sse-connect="/event_stream">\n' +
|
||||
' <div id="d1" sse-swap="e1"></div>\n' +
|
||||
' <div id="d2" sse-swap="e2"></div>\n' +
|
||||
'</div>\n');
|
||||
byId("d1").innerText.should.equal("")
|
||||
byId("d2").innerText.should.equal("")
|
||||
this.eventSource.sendEvent("e1", "Event 1")
|
||||
byId("d1").innerText.should.equal("Event 1")
|
||||
byId("d2").innerText.should.equal("")
|
||||
this.eventSource.sendEvent("e2", "Event 2")
|
||||
byId("d1").innerText.should.equal("Event 1")
|
||||
byId("d2").innerText.should.equal("Event 2")
|
||||
})
|
||||
|
||||
it('swaps swapped in content', function() {
|
||||
var div = make('<div hx-ext="sse" sse-connect="/event_stream">\n' +
|
||||
'<div id="d1" sse-swap="e1" hx-swap="outerHTML"></div>\n' +
|
||||
'</div>\n'
|
||||
)
|
||||
|
||||
this.eventSource.sendEvent("e1", '<div id="d2" sse-swap="e2"></div>')
|
||||
this.eventSource.sendEvent("e2", 'Event 2')
|
||||
byId("d2").innerText.should.equal("Event 2")
|
||||
})
|
||||
|
||||
it('works in a child of an hx-ext="sse" element', function() {
|
||||
var div = make('<div hx-ext="sse">\n' +
|
||||
'<div id="d1" sse-connect="/event_stream" sse-swap="e1">div1</div>\n' +
|
||||
'</div>\n'
|
||||
)
|
||||
this.eventSource.url = "/event_stream"
|
||||
})
|
||||
|
||||
it('only adds sseEventSource to elements with sse-connect', function() {
|
||||
var div = make('<div hx-ext="sse" sse-connect="/event_stream" >\n' +
|
||||
'<div id="d1" sse-swap="e1"></div>\n' +
|
||||
'</div>');
|
||||
|
||||
(byId('d1')["htmx-internal-data"].sseEventSource == undefined).should.be.true
|
||||
|
||||
// Even when content is swapped in
|
||||
this.eventSource.sendEvent("e1", '<div id="d2" sse-swap="e2"></div>');
|
||||
|
||||
(byId('d2')["htmx-internal-data"].sseEventSource == undefined).should.be.true
|
||||
})
|
||||
|
||||
it('initializes connections in swapped content', function() {
|
||||
this.server.respondWith("GET", "/d1", '<div><div sse-connect="/foo"><div id="d2" hx-trigger="sse:e2" hx-get="/d2">div2</div></div></div>');
|
||||
this.server.respondWith("GET", "/d2", "div2 updated");
|
||||
|
||||
var div = make('<div hx-ext="sse" hx-get="/d1"></div>');
|
||||
div.click();
|
||||
|
||||
this.server.respond();
|
||||
this.eventSource.sendEvent("e2");
|
||||
this.server.respond();
|
||||
|
||||
byId("d2").innerHTML.should.equal("div2 updated");
|
||||
})
|
||||
|
||||
it('creates an eventsource on elements with sse-connect', function() {
|
||||
var div = make('<div hx-ext="sse"><div id="d1"sse-connect="/event_stream"></div></div>');
|
||||
|
||||
(byId("d1")['htmx-internal-data'].sseEventSource == undefined).should.be.false;
|
||||
|
||||
})
|
||||
|
||||
});
|
||||
|
@ -158,6 +158,9 @@
|
||||
<script src="../src/ext/ws.js"></script>
|
||||
<script src="ext/ws.js"></script>
|
||||
|
||||
<script src="../src/ext/sse.js"></script>
|
||||
<script src="ext/sse.js"></script>
|
||||
|
||||
<script src="../src/ext/response-targets.js"></script>
|
||||
<script src="ext/response-targets.js"></script>
|
||||
|
||||
@ -177,7 +180,7 @@
|
||||
</script>
|
||||
<em>Work Area</em>
|
||||
<hr/>
|
||||
<div id="work-area" hx-history-elt hx-ext="sse">
|
||||
<div id="work-area" hx-history-elt>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user