mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-30 06:21:19 +00:00
Add events support for WebSockets (#1126)
* Add events support for WebSockets * Stop reconnection attempts after element has been removed * Add tests for websockets * Hide socker wrapper behind a more strict interface to avoid breaking changes in the future * Fix legacy websocket tests interfering with new extension tests * Minor doc fixes * Add `wsBinaryType` configuration option
This commit is contained in:
parent
8520f6f374
commit
36b017bc26
@ -44,6 +44,7 @@
|
||||
"mocha": "^7.2.0",
|
||||
"mocha-chrome": "^2.2.0",
|
||||
"mocha-webdriver-runner": "^0.6.3",
|
||||
"mock-socket": "^9.1.5",
|
||||
"sass": "^1.51.0",
|
||||
"sinon": "^9.2.4",
|
||||
"typescript": "^4.5.5",
|
||||
|
366
src/ext/ws.js
366
src/ext/ws.js
@ -4,7 +4,7 @@ WebSockets Extension
|
||||
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
|
||||
*/
|
||||
|
||||
(function(){
|
||||
(function () {
|
||||
|
||||
/** @type {import("../htmx").HtmxInternalApi} */
|
||||
var api;
|
||||
@ -15,18 +15,18 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
* init is called once, when this extension is first registered.
|
||||
* @param {import("../htmx").HtmxInternalApi} apiRef
|
||||
*/
|
||||
init: function(apiRef) {
|
||||
init: function (apiRef) {
|
||||
|
||||
// Store reference to internal API
|
||||
api = apiRef;
|
||||
|
||||
// Default function for creating new EventSource objects
|
||||
if (htmx.createWebSocket == undefined) {
|
||||
if (!htmx.createWebSocket) {
|
||||
htmx.createWebSocket = createWebSocket;
|
||||
}
|
||||
|
||||
// Default setting for reconnect delay
|
||||
if (htmx.config.wsReconnectDelay == undefined) {
|
||||
if (!htmx.config.wsReconnectDelay) {
|
||||
htmx.config.wsReconnectDelay = "full-jitter";
|
||||
}
|
||||
},
|
||||
@ -37,30 +37,30 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
* @param {string} name
|
||||
* @param {Event} evt
|
||||
*/
|
||||
onEvent: function(name, evt) {
|
||||
onEvent: function (name, evt) {
|
||||
|
||||
switch (name) {
|
||||
|
||||
// Try to remove remove an EventSource when elements are removed
|
||||
case "htmx:beforeCleanupElement":
|
||||
// Try to close the socket when elements are removed
|
||||
case "htmx:beforeCleanupElement":
|
||||
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
var internalData = api.getInternalData(evt.target)
|
||||
|
||||
if (internalData.webSocket != undefined) {
|
||||
internalData.webSocket.close();
|
||||
}
|
||||
return;
|
||||
if (internalData.webSocket) {
|
||||
internalData.webSocket.close();
|
||||
}
|
||||
return;
|
||||
|
||||
// Try to create EventSources when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
var parent = evt.target;
|
||||
// Try to create websockets when elements are processed
|
||||
case "htmx:afterProcessNode":
|
||||
var parent = evt.target;
|
||||
|
||||
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function(child) {
|
||||
ensureWebSocket(child)
|
||||
});
|
||||
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
|
||||
ensureWebSocketSend(child)
|
||||
});
|
||||
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
|
||||
ensureWebSocket(child)
|
||||
});
|
||||
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
|
||||
ensureWebSocketSend(child)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -85,23 +85,22 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
/**
|
||||
* ensureWebSocket creates a new WebSocket on the designated element, using
|
||||
* the element's "ws-connect" attribute.
|
||||
* @param {HTMLElement} elt
|
||||
* @param {number=} retryCount
|
||||
* @param {HTMLElement} socketElt
|
||||
* @returns
|
||||
*/
|
||||
function ensureWebSocket(elt, retryCount) {
|
||||
function ensureWebSocket(socketElt) {
|
||||
|
||||
// If the element containing the WebSocket connection no longer exists, then
|
||||
// do not connect/reconnect the WebSocket.
|
||||
if (!api.bodyContains(elt)) {
|
||||
if (!api.bodyContains(socketElt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the source straight from the element's value
|
||||
var wssSource = api.getAttributeValue(elt, "ws-connect")
|
||||
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
|
||||
|
||||
if (wssSource == null || wssSource === "") {
|
||||
var legacySource = getLegacyWebsocketURL(elt);
|
||||
var legacySource = getLegacyWebsocketURL(socketElt);
|
||||
if (legacySource == null) {
|
||||
return;
|
||||
} else {
|
||||
@ -109,58 +108,38 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
}
|
||||
}
|
||||
|
||||
// Default value for retryCount
|
||||
if (retryCount == undefined) {
|
||||
retryCount = 0;
|
||||
}
|
||||
|
||||
// Guarantee that the wssSource value is a fully qualified URL
|
||||
if (wssSource.indexOf("/") == 0) {
|
||||
var base_part = location.hostname + (location.port ? ':'+location.port: '');
|
||||
if (location.protocol == 'https:') {
|
||||
if (wssSource.indexOf("/") === 0) {
|
||||
var base_part = location.hostname + (location.port ? ':' + location.port : '');
|
||||
if (location.protocol === 'https:') {
|
||||
wssSource = "wss://" + base_part + wssSource;
|
||||
} else if (location.protocol == 'http:') {
|
||||
} else if (location.protocol === 'http:') {
|
||||
wssSource = "ws://" + base_part + wssSource;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new WebSocket and event handlers
|
||||
/** @type {WebSocket} */
|
||||
var socket = htmx.createWebSocket(wssSource);
|
||||
var socketWrapper = createWebsocketWrapper(socketElt, function () {
|
||||
return htmx.createWebSocket(wssSource)
|
||||
});
|
||||
|
||||
var messageQueue = [];
|
||||
|
||||
socket.onopen = function (e) {
|
||||
retryCount = 0;
|
||||
handleQueuedMessages(messageQueue, socket);
|
||||
}
|
||||
|
||||
socket.onclose = function (e) {
|
||||
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
|
||||
if ([1006, 1012, 1013].indexOf(e.code) >= 0) {
|
||||
var delay = getWebSocketReconnectDelay(retryCount);
|
||||
setTimeout(function() {
|
||||
ensureWebSocket(elt, retryCount+1);
|
||||
}, delay);
|
||||
}
|
||||
};
|
||||
|
||||
socket.onerror = function (e) {
|
||||
api.triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket});
|
||||
maybeCloseWebSocketSource(elt);
|
||||
};
|
||||
|
||||
socket.addEventListener('message', function (event) {
|
||||
if (maybeCloseWebSocketSource(elt)) {
|
||||
socketWrapper.addEventListener('message', function (event) {
|
||||
if (maybeCloseWebSocketSource(socketElt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var response = event.data;
|
||||
api.withExtensions(elt, function(extension){
|
||||
response = extension.transformResponse(response, null, elt);
|
||||
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
|
||||
message: response,
|
||||
socketWrapper: socketWrapper.publicInterface
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.withExtensions(socketElt, function (extension) {
|
||||
response = extension.transformResponse(response, null, socketElt);
|
||||
});
|
||||
|
||||
var settleInfo = api.makeSettleInfo(elt);
|
||||
var settleInfo = api.makeSettleInfo(socketElt);
|
||||
var fragment = api.makeFragment(response);
|
||||
|
||||
if (fragment.children.length) {
|
||||
@ -171,11 +150,148 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
}
|
||||
|
||||
api.settleImmediately(settleInfo.tasks);
|
||||
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
|
||||
});
|
||||
|
||||
// Put the WebSocket into the HTML Element's custom data.
|
||||
api.getInternalData(elt).webSocket = socket;
|
||||
api.getInternalData(elt).webSocketMessageQueue = messageQueue;
|
||||
api.getInternalData(socketElt).webSocket = socketWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} WebSocketWrapper
|
||||
* @property {WebSocket} socket
|
||||
* @property {Array<{message: string, sendElt: Element}>} messageQueue
|
||||
* @property {number} retryCount
|
||||
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
|
||||
* @property {(message: string, sendElt: Element) => void} send
|
||||
* @property {(event: string, handler: Function) => void} addEventListener
|
||||
* @property {() => void} handleQueuedMessages
|
||||
* @property {() => void} init
|
||||
* @property {() => void} close
|
||||
*/
|
||||
/**
|
||||
*
|
||||
* @param socketElt
|
||||
* @param socketFunc
|
||||
* @returns {WebSocketWrapper}
|
||||
*/
|
||||
function createWebsocketWrapper(socketElt, socketFunc) {
|
||||
var wrapper = {
|
||||
publicInterface: {
|
||||
send: this.send,
|
||||
sendImmediately: this.sendImmediately,
|
||||
queue: this.queue
|
||||
},
|
||||
socket: null,
|
||||
messageQueue: [],
|
||||
retryCount: 0,
|
||||
|
||||
/** @type {Object<string, Function[]>} */
|
||||
events: {},
|
||||
|
||||
addEventListener: function (event, handler) {
|
||||
if (this.socket) {
|
||||
this.socket.addEventListener(event, handler);
|
||||
}
|
||||
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = [];
|
||||
}
|
||||
|
||||
this.events[event].push(handler);
|
||||
},
|
||||
|
||||
sendImmediately: function (message, sendElt) {
|
||||
if (!this.socket) {
|
||||
api.triggerErrorEvent()
|
||||
}
|
||||
if (sendElt && api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
|
||||
message: message,
|
||||
socketWrapper: this.publicInterface
|
||||
})) {
|
||||
this.socket.send(message);
|
||||
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
|
||||
message: message,
|
||||
socketWrapper: this.publicInterface
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
send: function (message, sendElt) {
|
||||
if (this.socket.readyState !== this.socket.OPEN) {
|
||||
this.messageQueue.push({ message: message, sendElt: sendElt });
|
||||
} else {
|
||||
this.sendImmediately(message, sendElt);
|
||||
}
|
||||
},
|
||||
|
||||
handleQueuedMessages: function () {
|
||||
while (this.messageQueue.length > 0) {
|
||||
var queuedItem = this.messageQueue[0]
|
||||
if (this.socket.readyState === this.socket.OPEN) {
|
||||
this.sendImmediately(queuedItem.message, queuedItem.sendElt);
|
||||
this.messageQueue.shift();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
if (this.socket && this.socket.readyState === this.socket.OPEN) {
|
||||
// Close discarded socket
|
||||
this.socket.close()
|
||||
}
|
||||
|
||||
// Create a new WebSocket and event handlers
|
||||
/** @type {WebSocket} */
|
||||
var socket = socketFunc();
|
||||
|
||||
this.socket = socket;
|
||||
|
||||
socket.onopen = function (e) {
|
||||
wrapper.retryCount = 0;
|
||||
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
|
||||
wrapper.handleQueuedMessages();
|
||||
}
|
||||
|
||||
socket.onclose = function (e) {
|
||||
// If socket should not be connected, stop further attempts to establish connection
|
||||
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
|
||||
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
|
||||
var delay = getWebSocketReconnectDelay(wrapper.retryCount);
|
||||
setTimeout(function () {
|
||||
wrapper.retryCount += 1;
|
||||
wrapper.init();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// Notify client code that connection has been closed. Client code can inspect `event` field
|
||||
// to determine whether closure has been valid or abnormal
|
||||
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
|
||||
};
|
||||
|
||||
socket.onerror = function (e) {
|
||||
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
|
||||
maybeCloseWebSocketSource(socketElt);
|
||||
};
|
||||
|
||||
var events = this.events;
|
||||
Object.keys(events).forEach(function (k) {
|
||||
events[k].forEach(function (e) {
|
||||
socket.addEventListener(k, e);
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
close: function () {
|
||||
this.socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.init();
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -205,67 +321,65 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
/**
|
||||
* processWebSocketSend adds event listeners to the <form> element so that
|
||||
* messages can be sent to the WebSocket server when the form is submitted.
|
||||
* @param {HTMLElement} parent
|
||||
* @param {HTMLElement} child
|
||||
* @param {HTMLElement} socketElt
|
||||
* @param {HTMLElement} sendElt
|
||||
*/
|
||||
function processWebSocketSend(parent, child) {
|
||||
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;
|
||||
var rawParameters = results.values;
|
||||
var expressionVars = api.getExpressionVars(child);
|
||||
var allParameters = api.mergeObjects(rawParameters, expressionVars);
|
||||
var filteredParameters = api.filterValues(allParameters, child);
|
||||
filteredParameters['HEADERS'] = headers;
|
||||
if (errors && errors.length > 0) {
|
||||
api.triggerEvent(child, 'htmx:validation:halted', errors);
|
||||
function processWebSocketSend(socketElt, sendElt) {
|
||||
var nodeData = api.getInternalData(sendElt);
|
||||
var triggerSpecs = api.getTriggerSpecs(sendElt);
|
||||
triggerSpecs.forEach(function (ts) {
|
||||
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
|
||||
if (maybeCloseWebSocketSource(socketElt)) {
|
||||
return;
|
||||
}
|
||||
webSocketSend(webSocket, JSON.stringify(filteredParameters), messageQueue);
|
||||
if(api.shouldCancel(evt, child)){
|
||||
|
||||
/** @type {WebSocketWrapper} */
|
||||
var socketWrapper = api.getInternalData(socketElt).webSocket;
|
||||
var headers = api.getHeaders(sendElt, socketElt);
|
||||
var results = api.getInputValues(sendElt, 'post');
|
||||
var errors = results.errors;
|
||||
var rawParameters = results.values;
|
||||
var expressionVars = api.getExpressionVars(sendElt);
|
||||
var allParameters = api.mergeObjects(rawParameters, expressionVars);
|
||||
var filteredParameters = api.filterValues(allParameters, sendElt);
|
||||
|
||||
var sendConfig = {
|
||||
parameters: filteredParameters,
|
||||
unfilteredParameters: allParameters,
|
||||
headers: headers,
|
||||
errors: errors,
|
||||
|
||||
triggeringEvent: evt,
|
||||
messageBody: undefined,
|
||||
socketWrapper: socketWrapper.publicInterface
|
||||
};
|
||||
|
||||
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
api.triggerEvent(elt, 'htmx:validation:halted', errors);
|
||||
return;
|
||||
}
|
||||
|
||||
var body = sendConfig.messageBody;
|
||||
if (body === undefined) {
|
||||
var toSend = Object.assign({}, sendConfig.parameters);
|
||||
if (sendConfig.headers)
|
||||
toSend['HEADERS'] = headers;
|
||||
body = JSON.stringify(toSend);
|
||||
}
|
||||
|
||||
socketWrapper.send(body, elt);
|
||||
|
||||
if (api.shouldCancel(evt, elt)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
|
||||
* @param {number} retryCount // The number of retries that have already taken place
|
||||
@ -273,7 +387,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
*/
|
||||
function getWebSocketReconnectDelay(retryCount) {
|
||||
|
||||
/** @type {"full-jitter" | (retryCount:number) => number} */
|
||||
/** @type {"full-jitter" | ((retryCount:number) => number)} */
|
||||
var delay = htmx.config.wsReconnectDelay;
|
||||
if (typeof delay === 'function') {
|
||||
return delay(retryCount);
|
||||
@ -296,7 +410,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
* @param {*} elt
|
||||
* @returns
|
||||
*/
|
||||
function maybeCloseWebSocketSource(elt) {
|
||||
function maybeCloseWebSocketSource(elt) {
|
||||
if (!api.bodyContains(elt)) {
|
||||
api.getInternalData(elt).webSocket.close();
|
||||
return true;
|
||||
@ -311,8 +425,10 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
* @param {string} url
|
||||
* @returns WebSocket
|
||||
*/
|
||||
function createWebSocket(url){
|
||||
return new WebSocket(url, []);
|
||||
function createWebSocket(url) {
|
||||
var sock = new WebSocket(url, []);
|
||||
sock.binaryType = htmx.config.wsBinaryType;
|
||||
return sock;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -331,7 +447,7 @@ This extension adds support for WebSockets to htmx. See /www/extensions/ws.md f
|
||||
}
|
||||
|
||||
// Search all child nodes that match the requested attribute
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function(node) {
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
|
||||
result.push(node)
|
||||
})
|
||||
|
||||
|
@ -58,6 +58,7 @@ return (function () {
|
||||
withCredentials:false,
|
||||
timeout:0,
|
||||
wsReconnectDelay: 'full-jitter',
|
||||
wsBinaryType: 'blob',
|
||||
disableSelector: "[hx-disable], [data-hx-disable]",
|
||||
useTemplateFragments: false,
|
||||
scrollBehavior: 'smooth',
|
||||
@ -69,7 +70,9 @@ return (function () {
|
||||
return new EventSource(url, {withCredentials:true})
|
||||
},
|
||||
createWebSocket: function(url){
|
||||
return new WebSocket(url, []);
|
||||
var sock = new WebSocket(url, []);
|
||||
sock.binaryType = htmx.config.wsBinaryType;
|
||||
return sock;
|
||||
},
|
||||
version: "1.8.5"
|
||||
};
|
||||
|
@ -32,6 +32,7 @@ describe("hx-ws attribute", function() {
|
||||
var socket = mockWebsocket();
|
||||
this.socket = socket;
|
||||
clearWorkArea();
|
||||
this.oldCreateWebSocket = htmx.createWebSocket;
|
||||
htmx.createWebSocket = function(){
|
||||
return socket
|
||||
};
|
||||
@ -39,6 +40,7 @@ describe("hx-ws attribute", function() {
|
||||
afterEach(function () {
|
||||
this.server.restore();
|
||||
clearWorkArea();
|
||||
htmx.createWebSocket = this.oldCreateWebSocket;
|
||||
});
|
||||
|
||||
it('handles a basic call back', function () {
|
||||
|
295
test/ext/ws.js
Normal file
295
test/ext/ws.js
Normal file
@ -0,0 +1,295 @@
|
||||
describe("web-sockets extension", function () {
|
||||
beforeEach(function () {
|
||||
this.server = makeServer();
|
||||
this.socketServer = new Mock.Server('ws://localhost:8080');
|
||||
this.messages = [];
|
||||
this.clock = sinon.useFakeTimers();
|
||||
|
||||
this.socketServer.on('connection', function (socket) {
|
||||
socket.on('message', function (event) {
|
||||
this.messages.push(event)
|
||||
}.bind(this))
|
||||
}.bind(this))
|
||||
|
||||
/* Mock socket library is cool, but it uses setTimeout to emulate asynchronous nature of the network.
|
||||
* To avoid unexpected behavior, make sure to call this method whenever socket would have a network communication,
|
||||
* e.g., when connecting, disconnecting, sending messages. */
|
||||
this.tickMock = function () {
|
||||
this.clock.tick(5);
|
||||
}
|
||||
|
||||
clearWorkArea();
|
||||
});
|
||||
afterEach(function () {
|
||||
clearWorkArea();
|
||||
this.socketServer.close();
|
||||
this.socketServer.stop();
|
||||
this.clock.restore();
|
||||
});
|
||||
|
||||
it('can establish connection with the server', function () {
|
||||
this.socketServer.clients().length.should.equal(0);
|
||||
make('<div hx-ext="ws" ws-connect="ws://localhost:8080">');
|
||||
this.socketServer.clients().length.should.equal(1);
|
||||
|
||||
this.tickMock();
|
||||
})
|
||||
|
||||
it('is closed after removal by swap', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="ws" ws-connect="ws://localhost:8080">');
|
||||
this.tickMock();
|
||||
|
||||
this.socketServer.clients().length.should.equal(1);
|
||||
|
||||
div.click();
|
||||
this.server.respond();
|
||||
|
||||
this.tickMock();
|
||||
|
||||
this.socketServer.clients().length.should.equal(0);
|
||||
})
|
||||
|
||||
it('is closed after removal by js when message is received', function () {
|
||||
this.server.respondWith("GET", "/test", "Clicked!");
|
||||
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="ws" ws-connect="ws://localhost:8080">');
|
||||
this.tickMock();
|
||||
|
||||
this.socketServer.clients().length.should.equal(1);
|
||||
div.parentElement.removeChild(div);
|
||||
|
||||
this.socketServer.emit('message', 'foo');
|
||||
this.tickMock();
|
||||
|
||||
this.socketServer.clients().length.should.equal(0);
|
||||
})
|
||||
|
||||
it('sends data to the server', function () {
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
byId("d1").click();
|
||||
|
||||
this.tickMock();
|
||||
|
||||
this.messages.length.should.equal(1);
|
||||
})
|
||||
|
||||
it('handles message from the server', function () {
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
this.socketServer.emit('message', "<div id=\"d1\">replaced</div>")
|
||||
|
||||
this.tickMock();
|
||||
byId("d1").innerHTML.should.equal("replaced");
|
||||
byId("d2").innerHTML.should.equal("div2");
|
||||
})
|
||||
|
||||
it('raises event when socket connected', function () {
|
||||
var myEventCalled = false;
|
||||
var handler = function (evt) {
|
||||
myEventCalled = true;
|
||||
};
|
||||
htmx.on("htmx:wsOpen", handler)
|
||||
|
||||
make('<div hx-ext="ws" ws-connect="ws://localhost:8080">');
|
||||
this.tickMock();
|
||||
myEventCalled.should.be.true;
|
||||
htmx.off("htmx:wsOpen", handler)
|
||||
})
|
||||
|
||||
it('raises event when socket closed', function () {
|
||||
var myEventCalled = false;
|
||||
var handler = function (evt) {
|
||||
myEventCalled = true;
|
||||
};
|
||||
|
||||
var div = make('<div hx-get="/test" hx-swap="outerHTML" hx-ext="ws" ws-connect="ws://localhost:8080">');
|
||||
htmx.on(div, "htmx:wsClose", handler)
|
||||
this.tickMock();
|
||||
|
||||
div.parentElement.removeChild(div);
|
||||
|
||||
this.socketServer.emit('message', 'foo');
|
||||
this.tickMock();
|
||||
myEventCalled.should.be.true;
|
||||
this.tickMock();
|
||||
htmx.off(div, "htmx:wsClose", handler)
|
||||
})
|
||||
|
||||
it('raises htmx:wsConfig when sending, allows message modification', function () {
|
||||
var myEventCalled = false;
|
||||
|
||||
function handle(evt) {
|
||||
myEventCalled = true;
|
||||
evt.detail.parameters.foo = "bar";
|
||||
}
|
||||
|
||||
htmx.on("htmx:wsConfigSend", handle)
|
||||
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
byId("d1").click();
|
||||
|
||||
this.tickMock();
|
||||
|
||||
myEventCalled.should.be.true;
|
||||
this.messages.length.should.equal(1);
|
||||
this.messages[0].should.contains('"foo":"bar"')
|
||||
htmx.off("htmx:wsConfigSend", handle)
|
||||
})
|
||||
|
||||
it('cancels sending when htmx:wsConfigSend is cancelled', function () {
|
||||
var myEventCalled = false;
|
||||
|
||||
function handle(evt) {
|
||||
myEventCalled = true;
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
htmx.on("htmx:wsConfigSend", handle)
|
||||
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
byId("d1").click();
|
||||
|
||||
this.messages.length.should.equal(0);
|
||||
|
||||
myEventCalled.should.be.true;
|
||||
|
||||
htmx.off("htmx:wsConfigSend", handle);
|
||||
})
|
||||
|
||||
it('raises htmx:wsBeforeSend when sending', function () {
|
||||
var myEventCalled = false;
|
||||
|
||||
function handle(evt) {
|
||||
myEventCalled = true;
|
||||
}
|
||||
|
||||
htmx.on("htmx:wsBeforeSend", handle)
|
||||
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
byId("d1").click();
|
||||
|
||||
this.tickMock();
|
||||
|
||||
myEventCalled.should.be.true;
|
||||
this.messages.length.should.equal(1);
|
||||
htmx.off("htmx:wsBeforeSend", handle)
|
||||
})
|
||||
|
||||
it('cancels sending when htmx:wsBeforeSend is cancelled', function () {
|
||||
var myEventCalled = false;
|
||||
|
||||
function handle(evt) {
|
||||
myEventCalled = true;
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
htmx.on("htmx:wsBeforeSend", handle)
|
||||
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
byId("d1").click();
|
||||
|
||||
this.tickMock();
|
||||
|
||||
myEventCalled.should.be.true;
|
||||
this.messages.length.should.equal(0);
|
||||
htmx.off("htmx:wsBeforeSend", handle)
|
||||
})
|
||||
|
||||
it('raises htmx:wsAfterSend when sending', function () {
|
||||
var myEventCalled = false;
|
||||
|
||||
function handle(evt) {
|
||||
myEventCalled = true;
|
||||
}
|
||||
|
||||
htmx.on("htmx:wsAfterSend", handle)
|
||||
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div ws-send id="d1">div1</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
byId("d1").click();
|
||||
|
||||
this.tickMock();
|
||||
|
||||
myEventCalled.should.be.true;
|
||||
this.messages.length.should.equal(1);
|
||||
htmx.off("htmx:wsAfterSend", handle)
|
||||
})
|
||||
|
||||
it('raises htmx:wsBeforeMessage when receiving message from the server', function () {
|
||||
var myEventCalled = false;
|
||||
|
||||
function handle(evt) {
|
||||
myEventCalled = true;
|
||||
}
|
||||
|
||||
htmx.on("htmx:wsBeforeMessage", handle)
|
||||
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
this.socketServer.emit('message', "<div id=\"d1\">replaced</div>")
|
||||
|
||||
this.tickMock();
|
||||
myEventCalled.should.be.true;
|
||||
|
||||
htmx.off("htmx:wsBeforeMessage", handle)
|
||||
})
|
||||
|
||||
it('cancels swap when htmx:wsBeforeMessage was cancelled', function () {
|
||||
var myEventCalled = false;
|
||||
|
||||
function handle(evt) {
|
||||
myEventCalled = true;
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
htmx.on("htmx:wsBeforeMessage", handle)
|
||||
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
this.socketServer.emit('message', "<div id=\"d1\">replaced</div>")
|
||||
|
||||
this.tickMock();
|
||||
myEventCalled.should.be.true;
|
||||
|
||||
byId("d1").innerHTML.should.equal("div1");
|
||||
byId("d2").innerHTML.should.equal("div2");
|
||||
|
||||
htmx.off("htmx:wsBeforeMessage", handle)
|
||||
})
|
||||
|
||||
it('raises htmx:wsAfterMessage when message was completely processed', function () {
|
||||
var myEventCalled = false;
|
||||
|
||||
function handle(evt) {
|
||||
myEventCalled = true;
|
||||
}
|
||||
|
||||
htmx.on("htmx:wsAfterMessage", handle)
|
||||
|
||||
var div = make('<div hx-ext="ws" ws-connect="ws://localhost:8080"><div id="d1">div1</div><div id="d2">div2</div></div>');
|
||||
this.tickMock();
|
||||
|
||||
this.socketServer.emit('message', "<div id=\"d1\">replaced</div>")
|
||||
|
||||
this.tickMock();
|
||||
myEventCalled.should.be.true;
|
||||
|
||||
htmx.off("htmx:wsAfterMessage", handle)
|
||||
})
|
||||
});
|
@ -33,6 +33,7 @@
|
||||
<script src="../node_modules/mocha/mocha.js"></script>
|
||||
<script src="../node_modules/mocha-webdriver-runner/dist/mocha-webdriver-client.js"></script>
|
||||
<script src="../node_modules/sinon/pkg/sinon.js"></script>
|
||||
<script src="../node_modules/mock-socket/dist/mock-socket.js"></script>
|
||||
<script src="../src/htmx.js"></script>
|
||||
<script class="mocha-init">
|
||||
mocha.setup('bdd');
|
||||
@ -137,6 +138,9 @@
|
||||
<script src="../src/ext/multi-swap.js"></script>
|
||||
<script src="ext/multi-swap.js"></script>
|
||||
|
||||
<script src="../src/ext/ws.js"></script>
|
||||
<script src="ext/ws.js"></script>
|
||||
|
||||
<!-- events last so they don't screw up other tests -->
|
||||
<script src="core/events.js"></script>
|
||||
|
||||
@ -147,7 +151,7 @@
|
||||
</script>
|
||||
<em>Work Area</em>
|
||||
<hr/>
|
||||
<div id="work-area" hx-history-elt hx-ext="sse, ws">
|
||||
<div id="work-area" hx-history-elt hx-ext="sse">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,39 +1,58 @@
|
||||
---
|
||||
layout: layout.njk
|
||||
title: </> htmx - high power tools for html
|
||||
title: websockets extension - </> htmx - high power tools for html
|
||||
---
|
||||
|
||||
## The `WebSockets` Extension
|
||||
|
||||
The `WebSockets` extension enables easy, bi-directional communication with [Web Sockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications) servers directly from HTML. This replaces the experimental `hx-ws` attribute built into previous versions of htmx. For help migrating from older versions, see the [Migrating] guide at the bottom of this page.
|
||||
The `WebSockets` extension enables easy, bi-directional communication
|
||||
with [Web Sockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications)
|
||||
servers directly from HTML. This replaces the experimental `hx-ws` attribute built into previous versions of htmx. For
|
||||
help migrating from older versions, see the [Migrating] guide at the bottom of this page.
|
||||
|
||||
Use the following attributes to configure how WebSockets behave:
|
||||
|
||||
* `ws-connect="<url>"` or `ws-connect="<prefix>:<url>"` - A URL to establish an `WebSocket` connection against.
|
||||
* Prefixes `ws` or `wss` can optionally be specified. If not specified, HTMX defaults to add the location's scheme-type, host and port to have browsers send cookies via websockets.
|
||||
* `ws-send` - Sends a message to the nearest websocket based on the trigger value for the element (either the natural event
|
||||
of the event specified by [`hx-trigger`])
|
||||
* Prefixes `ws` or `wss` can optionally be specified. If not specified, HTMX defaults to add the location's scheme-type,
|
||||
host and port to have browsers send cookies via websockets.
|
||||
* `ws-send` - Sends a message to the nearest websocket based on the trigger value for the element (either the natural
|
||||
event
|
||||
of the event specified by [`hx-trigger`])
|
||||
|
||||
### Usage
|
||||
|
||||
```html
|
||||
<div hx-ext="ws" ws-connect="/chatroom">
|
||||
|
||||
<div hx-ext="ws" ws-connect="/chatroom">
|
||||
<div id="notifications"></div>
|
||||
<div id="chat_room">
|
||||
...
|
||||
...
|
||||
</div>
|
||||
<form id="form" ws-send>
|
||||
<input name="chat_message">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
WebSockets extension support two configuration options:
|
||||
|
||||
- `createWebSocket` - a factory function that can be used to create a custom WebSocket instances. Must be a function,
|
||||
returning `WebSocket` object
|
||||
- `wsBinaryType` - a string value, that defines
|
||||
socket's [`binaryType`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType) property. Default value
|
||||
is `blob`
|
||||
|
||||
### Receiving Messages from a WebSocket
|
||||
|
||||
The example above establishes a WebSocket to the `/chatroom` end point. Content that is sent down from the websocket will
|
||||
be parsed as HTML and swapped in by the `id` property, using the same logic as [Out of Band Swaps](/attributes/hx-swap-oob).
|
||||
The example above establishes a WebSocket to the `/chatroom` end point. Content that is sent down from the websocket
|
||||
will
|
||||
be parsed as HTML and swapped in by the `id` property, using the same logic
|
||||
as [Out of Band Swaps](/attributes/hx-swap-oob).
|
||||
|
||||
As such, if you want to change the swapping method (e.g., append content at the end of an element or delegate swapping to an extension),
|
||||
As such, if you want to change the swapping method (e.g., append content at the end of an element or delegate swapping
|
||||
to an extension),
|
||||
you need to specify that in the message body, sent by the server.
|
||||
|
||||
```html
|
||||
@ -53,7 +72,8 @@ you need to specify that in the message body, sent by the server.
|
||||
|
||||
### Sending Messages to a WebSocket
|
||||
|
||||
In the example above, the form uses the `ws-send` attribute to indicate that when it is submitted, the form values should be **serialized as JSON**
|
||||
In the example above, the form uses the `ws-send` attribute to indicate that when it is submitted, the form values
|
||||
should be **serialized as JSON**
|
||||
and send to the nearest enclosing `WebSocket`, in this case the `/chatroom` endpoint.
|
||||
|
||||
The serialized values will include a field, `HEADERS`, that includes the headers normally submitted with an htmx
|
||||
@ -61,32 +81,165 @@ request.
|
||||
|
||||
### Automatic Reconnection
|
||||
|
||||
If the WebSocket is closed unexpectedly, due to `Abnormal Closure`, `Service Restart` or `Try Again Later`, this extension will attempt to reconnect until the connection is reestablished.
|
||||
If the WebSocket is closed unexpectedly, due to `Abnormal Closure`, `Service Restart` or `Try Again Later`, this
|
||||
extension will attempt to reconnect until the connection is reestablished.
|
||||
|
||||
By default, the extension uses a full-jitter [exponential-backoff algorithm](https://en.wikipedia.org/wiki/Exponential_backoff) that chooses a randomized retry delay that grows exponentially over time. You can use a different algorithm by writing it into `htmx.config.wsReconnectDelay`. This function takes a single parameter, the number of retries, and returns the time (in milliseconds) to wait before trying again.
|
||||
By default, the extension uses a
|
||||
full-jitter [exponential-backoff algorithm](https://en.wikipedia.org/wiki/Exponential_backoff) that chooses a randomized
|
||||
retry delay that grows exponentially over time. You can use a different algorithm by writing it
|
||||
into `htmx.config.wsReconnectDelay`. This function takes a single parameter, the number of retries, and returns the
|
||||
time (in milliseconds) to wait before trying again.
|
||||
|
||||
```javascript
|
||||
// example reconnect delay that you shouldn't use because
|
||||
// it's not as good as the algorithm that's already in place
|
||||
htmx.config.wsReconnectDelay = function(retryCount) {
|
||||
htmx.config.wsReconnectDelay = function (retryCount) {
|
||||
return retryCount * 1000 // return value in milliseconds
|
||||
}
|
||||
```
|
||||
|
||||
The extension also implements a simple queuing mechanism that keeps messages in memory when the socket is not in `OPEN` state and sends them once the connection is restored.
|
||||
The extension also implements a simple queuing mechanism that keeps messages in memory when the socket is not in `OPEN`
|
||||
state and sends them once the connection is restored.
|
||||
|
||||
### Events
|
||||
|
||||
WebSockets extensions exposes a set of events that allow you to observe and customize its behavior.
|
||||
|
||||
#### <a name="htmx:wsOpen"></a> Event - [`htmx:wsOpen`](#htmx:wsOpen)
|
||||
|
||||
This event is triggered when a connection to WebSockets endpoint has been established.
|
||||
|
||||
##### Details
|
||||
|
||||
* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
|
||||
* `detail.event` - the original event from the socket
|
||||
* `detail.socketWrapper` - the wrapper around socket object
|
||||
|
||||
#### <a name="htmx:wsClose"></a> Event - [`htmx:wsClose`](#htmx:wsClose)
|
||||
|
||||
This event is triggered when a connection to WebSockets endpoint has been closed normally.
|
||||
You can check if the event was caused by an error by inspecting `detail.event` property.
|
||||
|
||||
##### Details
|
||||
|
||||
* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
|
||||
* `detail.event` - the original event from the socket
|
||||
* `detail.socketWrapper` - the wrapper around socket object
|
||||
|
||||
#### <a name="htmx:wsError"></a> Event - [`htmx:wsError`](#htmx:wsError)
|
||||
|
||||
This event is triggered when `onerror` event on a socket is raised.
|
||||
|
||||
##### Details
|
||||
|
||||
* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
|
||||
* `detail.error` - the error object
|
||||
* `detail.socketWrapper` - the wrapper around socket object
|
||||
|
||||
#### <a name="htmx:wsBeforeMessage"></a> Event - [`htmx:wsBeforeMessage`](#htmx:wsBeforeMessage)
|
||||
|
||||
This event is triggered when a message has just been received by a socket, similar to `htmx:beforeOnLoad`. This event
|
||||
fires
|
||||
before any processing occurs.
|
||||
|
||||
If the event is cancelled, no further processing will occur.
|
||||
|
||||
* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
|
||||
* `detail.message` - raw message content
|
||||
* `detail.socketWrapper` - the wrapper around socket object
|
||||
|
||||
#### <a name="htmx:wsAfterMessage"></a> Event - [`htmx:wsAfterMessage`](#htmx:wsAfterMessage)
|
||||
|
||||
This event is triggered when a message has been completely processed by htmx and all changes have been
|
||||
settled, similar to `htmx:afterOnLoad`.
|
||||
|
||||
Cancelling this event has no effect.
|
||||
|
||||
* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
|
||||
* `detail.message` - raw message content
|
||||
* `detail.socketWrapper` - the wrapper around socket object
|
||||
|
||||
#### <a name="htmx:wsConfigSend"></a> Event - [`htmx:wsConfigSend`](#htmx:wsConfigSend)
|
||||
|
||||
This event is triggered when preparing to send a message from `ws-send` element.
|
||||
Similarly to [`htmx:configRequest`](/events#htmx:configRequest), it allows you to modify the message
|
||||
before sending.
|
||||
|
||||
If the event is cancelled, no further processing will occur and no messages will be sent.
|
||||
|
||||
##### Details
|
||||
|
||||
* `detail.parameters` - the parameters that will be submitted in the request
|
||||
* `detail.unfilteredParameters` - the parameters that were found before filtering
|
||||
by [`hx-select`](/attributes/hx-select)
|
||||
* `detail.headers` - the request headers. Will be attached to the body in `HEADERS` property, if not falsy
|
||||
* `detail.errors` - validation errors. Will prevent sending and
|
||||
trigger [`htmx:validation:halted`](/events#htmx:validation:halted) event if not empty
|
||||
* `detail.triggeringEvent` - the event that triggered sending
|
||||
* `detail.messageBody` - raw message body that will be sent to the socket. Undefined, can be set to value of any type,
|
||||
supported by WebSockets. If set, will override
|
||||
default JSON serialization. Useful, if you want to use some other format, like XML or MessagePack
|
||||
* `detail.elt` - the element that dispatched the sending (the one with `ws-send` attribute)
|
||||
* `detail.socketWrapper` - the wrapper around socket object
|
||||
|
||||
#### <a name="htmx:wsBeforeSend"></a> Event - [`htmx:wsBeforeSend`](#htmx:wsBeforeSend)
|
||||
|
||||
This event is triggered just before sending a message. This includes messages from the queue.
|
||||
Message can not be modified at this point.
|
||||
|
||||
If the event is cancelled, the message will be discarded from the queue and not sent.
|
||||
|
||||
##### Details
|
||||
|
||||
* `detail.elt` - the element that dispatched the request (the one with `ws-connect` attribute)
|
||||
* `detail.message` - the raw message content
|
||||
* `detail.socketWrapper` - the wrapper around socket object
|
||||
|
||||
#### <a name="htmx:wsAfterSend"></a> Event - [`htmx:wsAfterSend`](#htmx:wsAfterSend)
|
||||
|
||||
This event is triggered just after sending a message. This includes messages from the queue.
|
||||
|
||||
Cancelling the event has no effect.
|
||||
|
||||
##### Details
|
||||
|
||||
* `detail.elt` - the element that dispatched the request (the one with `ws-connect` attribute)
|
||||
* `detail.message` - the raw message content
|
||||
* `detail.socketWrapper` - the wrapper around socket object
|
||||
|
||||
#### Socket wrapper
|
||||
|
||||
You may notice that all events expose `detail.socketWrapper` property. This wrapper holds the socket
|
||||
object itself and the message queue. It also encapsulates reconnection algorithm. It exposes a few members:
|
||||
|
||||
- `send(message, fromElt)` - sends a message safely. If the socket is not open, the message will be persisted in the
|
||||
queue
|
||||
instead and sent when the socket is ready.
|
||||
- `sendImmediately(message, fromElt)` - attempts to send a message regardless of socket state, bypassing the queue. May
|
||||
fail
|
||||
- `queue` - an array of messages, awaiting in the queue.
|
||||
|
||||
This wrapper can be used in your event handlers to monitor and manipulate the queue (e.g., you can reset the queue when
|
||||
reconnecting), and to send additional messages (e.g., if you want to send data in batches).
|
||||
The `fromElt` parameter is optional and, when specified, will trigger corresponding websocket events from
|
||||
specified element, namely `htmx:wsBeforeSend` and `htmx:wsAfterSend` events when sending your messages.
|
||||
|
||||
### Testing with the Demo Server
|
||||
|
||||
Htmx includes a demo WebSockets server written in Go that will help you to see WebSockets in action, and begin bootstrapping your own WebSockets code. It is located in the /test/servers/ws folder of the htmx distribution. Look at /test/servers/ws/README.md for instructions on running and using the test server.
|
||||
Htmx includes a demo WebSockets server written in Go that will help you to see WebSockets in action, and begin
|
||||
bootstrapping your own WebSockets code. It is located in the /test/servers/ws folder of the htmx distribution. Look at
|
||||
/test/servers/ws/README.md for instructions on running and using the test server.
|
||||
|
||||
### Migrating from Previous Versions
|
||||
|
||||
Previous versions of htmx used a built-in tag `hx-ws` to implement WebSockets. This code has been migrated into an extension instead. Here are the steps you need to take to migrate to this version:
|
||||
Previous versions of htmx used a built-in tag `hx-ws` to implement WebSockets. This code has been migrated into an
|
||||
extension instead. Here are the steps you need to take to migrate to this version:
|
||||
|
||||
| Old Attribute | New Attribute | Comments |
|
||||
|-------------------------|----------------------|-------------------|
|
||||
| `hx-ws=""` | `hx-ext="ws"` | Use the `hx-ext="ws"` attribute to install the WebSockets extension into any HTML element. |
|
||||
| Old Attribute | New Attribute | Comments |
|
||||
|-------------------------|----------------------|----------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `hx-ws=""` | `hx-ext="ws"` | Use the `hx-ext="ws"` attribute to install the WebSockets extension into any HTML element. |
|
||||
| `hx-ws="connect:<url>"` | `ws-connect="<url>"` | Add a new attribute `ws-connect` to the tag that defines the extension to specify the URL of the WebSockets server you're using. |
|
||||
| `hx-ws="send"` | `ws-send=""` | Add a new attribute `ws-send` to mark any child forms that should send data to your WebSocket server |
|
||||
| `hx-ws="send"` | `ws-send=""` | Add a new attribute `ws-send` to mark any child forms that should send data to your WebSocket server |
|
||||
|
||||
### Source
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user