mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-10-02 15:25:26 +00:00
Improvements in WebSocket extension
* Implement proper `hx-trigger` support * Expose trigger handling API to extensions * Implement safe message sending with sending queue * Fix `ws-send` attributes connecting in new elements * Fix OOB swapping of multiple elements in response
This commit is contained in:
parent
bcd68ad437
commit
8bb0399052
@ -58,6 +58,9 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function(child) {
|
||||
ensureWebSocket(child)
|
||||
});
|
||||
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
|
||||
ensureWebSocketSend(child)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -125,8 +128,11 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
/** @type {WebSocket} */
|
||||
var socket = htmx.createWebSocket(wssSource);
|
||||
|
||||
var messageQueue = [];
|
||||
|
||||
socket.onopen = function (e) {
|
||||
retryCount = 0;
|
||||
handleQueuedMessages(messageQueue, socket);
|
||||
}
|
||||
|
||||
socket.onclose = function (e) {
|
||||
@ -158,25 +164,42 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
var fragment = api.makeFragment(response);
|
||||
|
||||
if (fragment.children.length) {
|
||||
for (var i = 0; i < fragment.children.length; i++) {
|
||||
api.oobSwap(api.getAttributeValue(fragment.children[i], "hx-swap-oob") || "true", fragment.children[i], settleInfo);
|
||||
var children = Array.from(fragment.children);
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
|
||||
}
|
||||
}
|
||||
|
||||
api.settleImmediately(settleInfo.tasks);
|
||||
});
|
||||
|
||||
// Re-connect any ws-send commands as well.
|
||||
forEach(queryAttributeOnThisOrChildren(elt, "ws-send"), function(child) {
|
||||
var legacyAttribute = api.getAttributeValue(child, "hx-ws");
|
||||
// Put the WebSocket into the HTML Element's custom data.
|
||||
api.getInternalData(elt).webSocket = socket;
|
||||
api.getInternalData(elt).webSocketMessageQueue = messageQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* ensureWebSocketSend attaches trigger handles to elements with
|
||||
* "ws-send" attribute
|
||||
* @param {HTMLElement} elt
|
||||
*/
|
||||
function ensureWebSocketSend(elt) {
|
||||
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
|
||||
if (legacyAttribute && legacyAttribute !== 'send') {
|
||||
return;
|
||||
}
|
||||
processWebSocketSend(elt, child);
|
||||
});
|
||||
|
||||
// Put the WebSocket into the HTML Element's custom data.
|
||||
api.getInternalData(elt).webSocket = socket;
|
||||
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
|
||||
processWebSocketSend(webSocketParent, elt);
|
||||
}
|
||||
|
||||
/**
|
||||
* hasWebSocket function checks if a node has webSocket instance attached
|
||||
* @param {HTMLElement} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasWebSocket(node) {
|
||||
return api.getInternalData(node).webSocket != null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -186,8 +209,12 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
* @param {HTMLElement} child
|
||||
*/
|
||||
function processWebSocketSend(parent, child) {
|
||||
child.addEventListener(api.getTriggerSpecs(child)[0].trigger, function (evt) {
|
||||
var nodeData = api.getInternalData(child);
|
||||
let triggerSpecs = api.getTriggerSpecs(child);
|
||||
triggerSpecs.forEach(function(ts) {
|
||||
api.addTriggerHandler(child, ts, nodeData, function (evt) {
|
||||
var webSocket = api.getInternalData(parent).webSocket;
|
||||
var messageQueue = api.getInternalData(parent).webSocketMessageQueue;
|
||||
var headers = api.getHeaders(child, parent);
|
||||
var results = api.getInputValues(child, 'post');
|
||||
var errors = results.errors;
|
||||
@ -200,11 +227,43 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
api.triggerEvent(child, 'htmx:validation:halted', errors);
|
||||
return;
|
||||
}
|
||||
webSocket.send(JSON.stringify(filteredParameters));
|
||||
webSocketSend(webSocket, JSON.stringify(filteredParameters), messageQueue);
|
||||
if(api.shouldCancel(evt, child)){
|
||||
evt.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* webSocketSend provides a safe way to send messages through a WebSocket.
|
||||
* It checks that the socket is in OPEN state and, otherwise, awaits for it.
|
||||
* @param {WebSocket} socket
|
||||
* @param {string} message
|
||||
* @param {string[]} messageQueue
|
||||
* @return {boolean}
|
||||
*/
|
||||
function webSocketSend(socket, message, messageQueue) {
|
||||
if (socket.readyState != socket.OPEN) {
|
||||
messageQueue.push(message);
|
||||
} else {
|
||||
socket.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handleQueuedMessages sends messages awaiting in the message queue
|
||||
*/
|
||||
function handleQueuedMessages(messageQueue, socket) {
|
||||
while (messageQueue.length > 0) {
|
||||
var message = messageQueue[0]
|
||||
if (socket.readyState == socket.OPEN) {
|
||||
socket.send(message);
|
||||
messageQueue.shift()
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -293,3 +352,4 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
|
81
src/htmx.js
81
src/htmx.js
@ -76,6 +76,7 @@ return (function () {
|
||||
|
||||
/** @type {import("./htmx").HtmxInternalApi} */
|
||||
var internalAPI = {
|
||||
addTriggerHandler: addTriggerHandler,
|
||||
bodyContains: bodyContains,
|
||||
canAccessLocalStorage: canAccessLocalStorage,
|
||||
filterValues: filterValues,
|
||||
@ -172,13 +173,11 @@ return (function () {
|
||||
* @returns {HTMLElement | null}
|
||||
*/
|
||||
function getClosestMatch(elt, condition) {
|
||||
if (condition(elt)) {
|
||||
return elt;
|
||||
} else if (parentElt(elt)) {
|
||||
return getClosestMatch(parentElt(elt), condition);
|
||||
} else {
|
||||
return null;
|
||||
while (elt && !condition(elt)) {
|
||||
elt = parentElt(elt);
|
||||
}
|
||||
|
||||
return elt ? elt : null;
|
||||
}
|
||||
|
||||
function getAttributeValueWithDisinheritance(initialElement, ancestor, attributeName){
|
||||
@ -1188,14 +1187,14 @@ return (function () {
|
||||
getInternalData(elt).cancelled = true;
|
||||
}
|
||||
|
||||
function processPolling(elt, verb, path, spec) {
|
||||
function processPolling(elt, handler, spec) {
|
||||
var nodeData = getInternalData(elt);
|
||||
nodeData.timeout = setTimeout(function () {
|
||||
if (bodyContains(elt) && nodeData.cancelled !== true) {
|
||||
if (!maybeFilterEvent(spec, makeEvent('hx:poll:trigger', {triggerSpec:spec, target:elt}))) {
|
||||
issueAjaxRequest(verb, path, elt);
|
||||
handler(elt);
|
||||
}
|
||||
processPolling(elt, verb, getAttributeValue(elt, "hx-" + verb), spec);
|
||||
processPolling(elt, handler, spec);
|
||||
}
|
||||
}, spec.pollInterval);
|
||||
}
|
||||
@ -1223,7 +1222,9 @@ return (function () {
|
||||
path = getRawAttribute(elt, 'action');
|
||||
}
|
||||
triggerSpecs.forEach(function(triggerSpec) {
|
||||
addEventListener(elt, verb, path, nodeData, triggerSpec, true);
|
||||
addEventListener(elt, function(evt) {
|
||||
issueAjaxRequest(verb, path, elt, evt)
|
||||
}, nodeData, triggerSpec, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1267,7 +1268,7 @@ return (function () {
|
||||
return false;
|
||||
}
|
||||
|
||||
function addEventListener(elt, verb, path, nodeData, triggerSpec, explicitCancel) {
|
||||
function addEventListener(elt, handler, nodeData, triggerSpec, explicitCancel) {
|
||||
var eltsToListenOn;
|
||||
if (triggerSpec.from) {
|
||||
eltsToListenOn = querySelectorAllExt(elt, triggerSpec.from);
|
||||
@ -1328,17 +1329,15 @@ return (function () {
|
||||
|
||||
if (triggerSpec.throttle) {
|
||||
if (!elementData.throttle) {
|
||||
issueAjaxRequest(verb, path, elt, evt);
|
||||
handler(elt, evt);
|
||||
elementData.throttle = setTimeout(function () {
|
||||
elementData.throttle = null;
|
||||
}, triggerSpec.throttle);
|
||||
}
|
||||
} else if (triggerSpec.delay) {
|
||||
elementData.delayed = setTimeout(function () {
|
||||
issueAjaxRequest(verb, path, elt, evt);
|
||||
}, triggerSpec.delay);
|
||||
elementData.delayed = setTimeout(function() { handler(elt, evt) }, triggerSpec.delay);
|
||||
} else {
|
||||
issueAjaxRequest(verb, path, elt, evt);
|
||||
handler(elt, evt);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -1356,7 +1355,7 @@ return (function () {
|
||||
|
||||
var windowIsScrolling = false // used by initScrollHandler
|
||||
var scrollHandler = null;
|
||||
function initScrollHandler() {
|
||||
function initScrollHandler(handler) {
|
||||
if (!scrollHandler) {
|
||||
scrollHandler = function() {
|
||||
windowIsScrolling = true
|
||||
@ -1366,25 +1365,22 @@ return (function () {
|
||||
if (windowIsScrolling) {
|
||||
windowIsScrolling = false;
|
||||
forEach(getDocument().querySelectorAll("[hx-trigger='revealed'],[data-hx-trigger='revealed']"), function (elt) {
|
||||
maybeReveal(elt);
|
||||
maybeReveal(elt, handler);
|
||||
})
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeReveal(elt) {
|
||||
function maybeReveal(elt, handler) {
|
||||
if (!hasAttribute(elt,'data-hx-revealed') && isScrolledIntoView(elt)) {
|
||||
elt.setAttribute('data-hx-revealed', 'true');
|
||||
var nodeData = getInternalData(elt);
|
||||
if (nodeData.initialized) {
|
||||
issueAjaxRequest(nodeData.verb, nodeData.path, elt);
|
||||
handler(elt);
|
||||
} else {
|
||||
// if the node isn't initialized, wait for it before triggering the request
|
||||
elt.addEventListener("htmx:afterProcessNode",
|
||||
function () {
|
||||
issueAjaxRequest(nodeData.verb, nodeData.path, elt);
|
||||
}, {once: true});
|
||||
elt.addEventListener("htmx:afterProcessNode", function(evt) { handler(elt) }, {once: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1571,14 +1567,14 @@ return (function () {
|
||||
}
|
||||
}
|
||||
|
||||
function processSSETrigger(elt, verb, path, sseEventName) {
|
||||
function processSSETrigger(elt, handler, sseEventName) {
|
||||
var sseSourceElt = getClosestMatch(elt, hasEventSource);
|
||||
if (sseSourceElt) {
|
||||
var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
|
||||
var sseListener = function () {
|
||||
if (!maybeCloseSSESource(sseSourceElt)) {
|
||||
if (bodyContains(elt)) {
|
||||
issueAjaxRequest(verb, path, elt);
|
||||
handler(elt);
|
||||
} else {
|
||||
sseEventSource.removeEventListener(sseEventName, sseListener);
|
||||
}
|
||||
@ -1604,11 +1600,11 @@ return (function () {
|
||||
|
||||
//====================================================================
|
||||
|
||||
function loadImmediately(elt, verb, path, nodeData, delay) {
|
||||
function loadImmediately(elt, handler, nodeData, delay) {
|
||||
var load = function(){
|
||||
if (!nodeData.loaded) {
|
||||
nodeData.loaded = true;
|
||||
issueAjaxRequest(verb, path, elt);
|
||||
handler(elt);
|
||||
}
|
||||
}
|
||||
if (delay) {
|
||||
@ -1627,11 +1623,21 @@ return (function () {
|
||||
nodeData.path = path;
|
||||
nodeData.verb = verb;
|
||||
triggerSpecs.forEach(function(triggerSpec) {
|
||||
addTriggerHandler(elt, triggerSpec, nodeData, function (elt, evt) {
|
||||
issueAjaxRequest(verb, path, elt, evt)
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
return explicitAction;
|
||||
}
|
||||
|
||||
function addTriggerHandler(elt, triggerSpec, nodeData, handler) {
|
||||
if (triggerSpec.sseEvent) {
|
||||
processSSETrigger(elt, verb, path, triggerSpec.sseEvent);
|
||||
processSSETrigger(elt, handler, triggerSpec.sseEvent);
|
||||
} else if (triggerSpec.trigger === "revealed") {
|
||||
initScrollHandler();
|
||||
maybeReveal(elt);
|
||||
initScrollHandler(handler);
|
||||
maybeReveal(elt, handler);
|
||||
} else if (triggerSpec.trigger === "intersect") {
|
||||
var observerOptions = {};
|
||||
if (triggerSpec.root) {
|
||||
@ -1650,21 +1656,17 @@ return (function () {
|
||||
}
|
||||
}, observerOptions);
|
||||
observer.observe(elt);
|
||||
addEventListener(elt, verb, path, nodeData, triggerSpec);
|
||||
addEventListener(elt, handler, nodeData, triggerSpec);
|
||||
} else if (triggerSpec.trigger === "load") {
|
||||
if (!maybeFilterEvent(triggerSpec, makeEvent("load", {elt:elt}))) {
|
||||
loadImmediately(elt, verb, path, nodeData, triggerSpec.delay);
|
||||
loadImmediately(elt, handler, nodeData, triggerSpec.delay);
|
||||
}
|
||||
} else if (triggerSpec.pollInterval) {
|
||||
nodeData.polling = true;
|
||||
processPolling(elt, verb, path, triggerSpec);
|
||||
processPolling(elt, handler, triggerSpec);
|
||||
} else {
|
||||
addEventListener(elt, verb, path, nodeData, triggerSpec);
|
||||
addEventListener(elt, handler, nodeData, triggerSpec);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return explicitAction;
|
||||
}
|
||||
|
||||
function evalScript(script) {
|
||||
@ -3177,3 +3179,4 @@ return (function () {
|
||||
}
|
||||
)()
|
||||
}));
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user