mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-10-02 15:25:26 +00:00
Restore hx-ws and hx-sse tags (#811)
absolute 👑
* Restore WS and SSE code
First pass at restoring removed ws and sse code. More to come.
* More progress on WS and SSE restore
* Update htmx.js
crucial whitespace
* Update documentation
* Combine SSE and WS servers into single "realtime" demo
* Realtime Test Server Updates
- separated tests for old- and new- style calling
- updated intro content and stylesheet
- removed extensions from manual test suite
* Remove SSE/WS from manual tests
This commit is contained in:
parent
eb1367fb11
commit
546e346e98
245
src/htmx.js
245
src/htmx.js
@ -57,12 +57,19 @@ return (function () {
|
|||||||
attributesToSettle:["class", "style", "width", "height"],
|
attributesToSettle:["class", "style", "width", "height"],
|
||||||
withCredentials:false,
|
withCredentials:false,
|
||||||
timeout:0,
|
timeout:0,
|
||||||
|
wsReconnectDelay: 'full-jitter',
|
||||||
disableSelector: "[hx-disable], [data-hx-disable]",
|
disableSelector: "[hx-disable], [data-hx-disable]",
|
||||||
useTemplateFragments: false,
|
useTemplateFragments: false,
|
||||||
scrollBehavior: 'smooth',
|
scrollBehavior: 'smooth',
|
||||||
},
|
},
|
||||||
parseInterval:parseInterval,
|
parseInterval:parseInterval,
|
||||||
_:internalEval,
|
_:internalEval,
|
||||||
|
createEventSource: function(url){
|
||||||
|
return new EventSource(url, {withCredentials:true})
|
||||||
|
},
|
||||||
|
createWebSocket: function(url){
|
||||||
|
return new WebSocket(url, []);
|
||||||
|
},
|
||||||
version: "1.7.0"
|
version: "1.7.0"
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -562,6 +569,7 @@ return (function () {
|
|||||||
//====================================================================
|
//====================================================================
|
||||||
// Node processing
|
// Node processing
|
||||||
//====================================================================
|
//====================================================================
|
||||||
|
|
||||||
var DUMMY_ELT = getDocument().createElement("output"); // dummy element for bad selectors
|
var DUMMY_ELT = getDocument().createElement("output"); // dummy element for bad selectors
|
||||||
function findAttributeTargets(elt, attrName) {
|
function findAttributeTargets(elt, attrName) {
|
||||||
var attrTarget = getClosestAttributeValue(elt, attrName);
|
var attrTarget = getClosestAttributeValue(elt, attrName);
|
||||||
@ -760,6 +768,12 @@ return (function () {
|
|||||||
|
|
||||||
function cleanUpElement(element) {
|
function cleanUpElement(element) {
|
||||||
var internalData = getInternalData(element);
|
var internalData = getInternalData(element);
|
||||||
|
if (internalData.webSocket) {
|
||||||
|
internalData.webSocket.close();
|
||||||
|
}
|
||||||
|
if (internalData.sseEventSource) {
|
||||||
|
internalData.sseEventSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
triggerEvent(element, "htmx:beforeCleanupElement")
|
triggerEvent(element, "htmx:beforeCleanupElement")
|
||||||
|
|
||||||
@ -1066,6 +1080,8 @@ return (function () {
|
|||||||
every.eventFilter = eventFilter;
|
every.eventFilter = eventFilter;
|
||||||
}
|
}
|
||||||
triggerSpecs.push(every);
|
triggerSpecs.push(every);
|
||||||
|
} else if (trigger.indexOf("sse:") === 0) {
|
||||||
|
triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
|
||||||
} else {
|
} else {
|
||||||
var triggerSpec = {trigger: trigger};
|
var triggerSpec = {trigger: trigger};
|
||||||
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
var eventFilter = maybeGenerateConditional(elt, tokens, "event");
|
||||||
@ -1339,6 +1355,219 @@ return (function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//====================================================================
|
||||||
|
// Web Sockets
|
||||||
|
//====================================================================
|
||||||
|
|
||||||
|
function processWebSocketInfo(elt, nodeData, info) {
|
||||||
|
var values = splitOnWhitespace(info);
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
var value = values[i].split(/:(.+)/);
|
||||||
|
if (value[0] === "connect") {
|
||||||
|
ensureWebSocket(elt, value[1], 0);
|
||||||
|
}
|
||||||
|
if (value[0] === "send") {
|
||||||
|
processWebSocketSend(elt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureWebSocket(elt, wssSource, retryCount) {
|
||||||
|
if (!bodyContains(elt)) {
|
||||||
|
return; // stop ensuring websocket connection when socket bearing element ceases to exist
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wssSource.indexOf("/") == 0) { // complete absolute paths only
|
||||||
|
var base_part = location.hostname + (location.port ? ':'+location.port: '');
|
||||||
|
if (location.protocol == 'https:') {
|
||||||
|
wssSource = "wss://" + base_part + wssSource;
|
||||||
|
} else if (location.protocol == 'http:') {
|
||||||
|
wssSource = "ws://" + base_part + wssSource;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var socket = htmx.createWebSocket(wssSource);
|
||||||
|
socket.onerror = function (e) {
|
||||||
|
triggerErrorEvent(elt, "htmx:wsError", {error:e, socket:socket});
|
||||||
|
maybeCloseWebSocketSource(elt);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = function (e) {
|
||||||
|
if ([1006, 1012, 1013].indexOf(e.code) >= 0) { // Abnormal Closure/Service Restart/Try Again Later
|
||||||
|
var delay = getWebSocketReconnectDelay(retryCount);
|
||||||
|
setTimeout(function() {
|
||||||
|
ensureWebSocket(elt, wssSource, retryCount+1); // creates a websocket with a new timeout
|
||||||
|
}, delay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
socket.onopen = function (e) {
|
||||||
|
retryCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInternalData(elt).webSocket = socket;
|
||||||
|
socket.addEventListener('message', function (event) {
|
||||||
|
if (maybeCloseWebSocketSource(elt)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = event.data;
|
||||||
|
withExtensions(elt, function(extension){
|
||||||
|
response = extension.transformResponse(response, null, elt);
|
||||||
|
});
|
||||||
|
|
||||||
|
var settleInfo = makeSettleInfo(elt);
|
||||||
|
var fragment = makeFragment(response);
|
||||||
|
var children = toArray(fragment.children);
|
||||||
|
for (var i = 0; i < children.length; i++) {
|
||||||
|
var child = children[i];
|
||||||
|
oobSwap(getAttributeValue(child, "hx-swap-oob") || "true", child, settleInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
settleImmediately(settleInfo.tasks);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeCloseWebSocketSource(elt) {
|
||||||
|
if (!bodyContains(elt)) {
|
||||||
|
getInternalData(elt).webSocket.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processWebSocketSend(elt) {
|
||||||
|
var webSocketSourceElt = getClosestMatch(elt, function (parent) {
|
||||||
|
return getInternalData(parent).webSocket != null;
|
||||||
|
});
|
||||||
|
if (webSocketSourceElt) {
|
||||||
|
elt.addEventListener(getTriggerSpecs(elt)[0].trigger, function (evt) {
|
||||||
|
var webSocket = getInternalData(webSocketSourceElt).webSocket;
|
||||||
|
var headers = getHeaders(elt, webSocketSourceElt);
|
||||||
|
var results = getInputValues(elt, 'post');
|
||||||
|
var errors = results.errors;
|
||||||
|
var rawParameters = results.values;
|
||||||
|
var expressionVars = getExpressionVars(elt);
|
||||||
|
var allParameters = mergeObjects(rawParameters, expressionVars);
|
||||||
|
var filteredParameters = filterValues(allParameters, elt);
|
||||||
|
filteredParameters['HEADERS'] = headers;
|
||||||
|
if (errors && errors.length > 0) {
|
||||||
|
triggerEvent(elt, 'htmx:validation:halted', errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
webSocket.send(JSON.stringify(filteredParameters));
|
||||||
|
if(shouldCancel(evt, elt)){
|
||||||
|
evt.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
triggerErrorEvent(elt, "htmx:noWebSocketSourceError");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebSocketReconnectDelay(retryCount) {
|
||||||
|
var delay = htmx.config.wsReconnectDelay;
|
||||||
|
if (typeof delay === 'function') {
|
||||||
|
// @ts-ignore
|
||||||
|
return delay(retryCount);
|
||||||
|
}
|
||||||
|
if (delay === 'full-jitter') {
|
||||||
|
var exp = Math.min(retryCount, 6);
|
||||||
|
var maxDelay = 1000 * Math.pow(2, exp);
|
||||||
|
return maxDelay * Math.random();
|
||||||
|
}
|
||||||
|
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
|
||||||
|
}
|
||||||
|
|
||||||
|
//====================================================================
|
||||||
|
// Server Sent Events
|
||||||
|
//====================================================================
|
||||||
|
|
||||||
|
function processSSEInfo(elt, nodeData, info) {
|
||||||
|
var values = splitOnWhitespace(info);
|
||||||
|
for (var i = 0; i < values.length; i++) {
|
||||||
|
var value = values[i].split(/:(.+)/);
|
||||||
|
if (value[0] === "connect") {
|
||||||
|
processSSESource(elt, value[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((value[0] === "swap")) {
|
||||||
|
processSSESwap(elt, value[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processSSESource(elt, sseSrc) {
|
||||||
|
var source = htmx.createEventSource(sseSrc);
|
||||||
|
source.onerror = function (e) {
|
||||||
|
triggerErrorEvent(elt, "htmx:sseError", {error:e, source:source});
|
||||||
|
maybeCloseSSESource(elt);
|
||||||
|
};
|
||||||
|
getInternalData(elt).sseEventSource = source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processSSESwap(elt, sseEventName) {
|
||||||
|
var sseSourceElt = getClosestMatch(elt, hasEventSource);
|
||||||
|
if (sseSourceElt) {
|
||||||
|
var sseEventSource = getInternalData(sseSourceElt).sseEventSource;
|
||||||
|
var sseListener = function (event) {
|
||||||
|
if (maybeCloseSSESource(sseSourceElt)) {
|
||||||
|
sseEventSource.removeEventListener(sseEventName, sseListener);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////
|
||||||
|
// TODO: merge this code with AJAX and WebSockets code in the future.
|
||||||
|
|
||||||
|
var response = event.data;
|
||||||
|
withExtensions(elt, function(extension){
|
||||||
|
response = extension.transformResponse(response, null, elt);
|
||||||
|
});
|
||||||
|
|
||||||
|
var swapSpec = getSwapSpecification(elt)
|
||||||
|
var target = getTarget(elt)
|
||||||
|
var settleInfo = makeSettleInfo(elt);
|
||||||
|
|
||||||
|
selectAndSwap(swapSpec.swapStyle, elt, target, response, settleInfo)
|
||||||
|
settleImmediately(settleInfo.tasks)
|
||||||
|
triggerEvent(elt, "htmx:sseMessage", event)
|
||||||
|
};
|
||||||
|
|
||||||
|
getInternalData(elt).sseListener = sseListener;
|
||||||
|
sseEventSource.addEventListener(sseEventName, sseListener);
|
||||||
|
} else {
|
||||||
|
triggerErrorEvent(elt, "htmx:noSSESourceError");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processSSETrigger(elt, verb, path, 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);
|
||||||
|
} else {
|
||||||
|
sseEventSource.removeEventListener(sseEventName, sseListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getInternalData(elt).sseListener = sseListener;
|
||||||
|
sseEventSource.addEventListener(sseEventName, sseListener);
|
||||||
|
} else {
|
||||||
|
triggerErrorEvent(elt, "htmx:noSSESourceError");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeCloseSSESource(elt) {
|
||||||
|
if (!bodyContains(elt)) {
|
||||||
|
getInternalData(elt).sseEventSource.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasEventSource(node) {
|
||||||
|
return getInternalData(node).sseEventSource != null;
|
||||||
|
}
|
||||||
|
|
||||||
//====================================================================
|
//====================================================================
|
||||||
|
|
||||||
function loadImmediately(elt, verb, path, nodeData, delay) {
|
function loadImmediately(elt, verb, path, nodeData, delay) {
|
||||||
@ -1364,7 +1593,9 @@ return (function () {
|
|||||||
nodeData.path = path;
|
nodeData.path = path;
|
||||||
nodeData.verb = verb;
|
nodeData.verb = verb;
|
||||||
triggerSpecs.forEach(function(triggerSpec) {
|
triggerSpecs.forEach(function(triggerSpec) {
|
||||||
if (triggerSpec.trigger === "revealed") {
|
if (triggerSpec.sseEvent) {
|
||||||
|
processSSETrigger(elt, verb, path, triggerSpec.sseEvent);
|
||||||
|
} else if (triggerSpec.trigger === "revealed") {
|
||||||
initScrollHandler();
|
initScrollHandler();
|
||||||
maybeReveal(elt);
|
maybeReveal(elt);
|
||||||
} else if (triggerSpec.trigger === "intersect") {
|
} else if (triggerSpec.trigger === "intersect") {
|
||||||
@ -1439,7 +1670,8 @@ return (function () {
|
|||||||
function findElementsToProcess(elt) {
|
function findElementsToProcess(elt) {
|
||||||
if (elt.querySelectorAll) {
|
if (elt.querySelectorAll) {
|
||||||
var boostedElts = isBoosted() ? ", a, form" : "";
|
var boostedElts = isBoosted() ? ", a, form" : "";
|
||||||
var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", [hx-ext], [data-hx-ext]");
|
var results = elt.querySelectorAll(VERB_SELECTOR + boostedElts + ", [hx-sse], [data-hx-sse], [hx-ws]," +
|
||||||
|
" [data-hx-ws], [hx-ext], [hx-data-ext]");
|
||||||
return results;
|
return results;
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
@ -1490,6 +1722,15 @@ return (function () {
|
|||||||
initButtonTracking(elt);
|
initButtonTracking(elt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sseInfo = getAttributeValue(elt, 'hx-sse');
|
||||||
|
if (sseInfo) {
|
||||||
|
processSSEInfo(elt, nodeData, sseInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
var wsInfo = getAttributeValue(elt, 'hx-ws');
|
||||||
|
if (wsInfo) {
|
||||||
|
processWebSocketInfo(elt, nodeData, wsInfo);
|
||||||
|
}
|
||||||
triggerEvent(elt, "htmx:afterProcessNode");
|
triggerEvent(elt, "htmx:afterProcessNode");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,9 +126,6 @@
|
|||||||
<script src="../src/ext/event-header.js"></script>
|
<script src="../src/ext/event-header.js"></script>
|
||||||
<script src="ext/event-header.js"></script>
|
<script src="ext/event-header.js"></script>
|
||||||
|
|
||||||
<script src="../src/ext/sse.js"></script>
|
|
||||||
<script src="../src/ext/ws.js"></script>
|
|
||||||
|
|
||||||
<!-- events last so they don't screw up other tests -->
|
<!-- events last so they don't screw up other tests -->
|
||||||
<script src="core/events.js"></script>
|
<script src="core/events.js"></script>
|
||||||
|
|
||||||
|
@ -24,19 +24,6 @@
|
|||||||
<li><a href="scroll-test-targets.html">Targets</a></li>
|
<li><a href="scroll-test-targets.html">Targets</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>SSE
|
|
||||||
<ul>
|
|
||||||
<li><a href="sse.html">Core SSE Test</a></li>
|
|
||||||
<li><a href="sse-multichannel.html">SSE Multichannel</a></li>
|
|
||||||
<li><a href="sse-multichannel.html">SSE Triggers</a></li>
|
|
||||||
<li><a href="sse-settle.html">SSE Settle</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>Websocket
|
|
||||||
<ul>
|
|
||||||
<li><a href="websocket-reconnect.html">Reconnect</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li>History
|
<li>History
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="history">Core History Test</a></li>
|
<li><a href="history">Core History Test</a></li>
|
||||||
|
57
test/realtime/README.md
Normal file
57
test/realtime/README.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Htmx - Realtime Test Suite
|
||||||
|
|
||||||
|
This package implements a realtime server for testing WebSockets and Server Sent Events (SSE) in htmx.
|
||||||
|
|
||||||
|
## How to Use This Server
|
||||||
|
|
||||||
|
1. If you do not already have Go (version 1.17 or higher) installed on your machine, you can download an installation for your machine from [https://golang.org](the Go website)
|
||||||
|
|
||||||
|
2. Open up a terminal window and navigate to this directory. Start up the WebSocket server by typing `go run server.go`
|
||||||
|
|
||||||
|
3. Your browser should open the test suite web page automatically. If it doesn't, then navigate to [http://localhost](http://localhost) to run the manual tests. Huzzah!
|
||||||
|
|
||||||
|
## Web Sockets
|
||||||
|
|
||||||
|
This listens for incoming WebSocket connections coming in to ws://localhost:1323/echo and ws://localhost:1323/heartbeat. When it receives messages from any WebSocket client, it responds with that same content in a way that htmx can process. This means, that the response message will look like this: `<div id="idMessage" hx-swap-oob="true">{your message here}</div>`
|
||||||
|
|
||||||
|
### Echo
|
||||||
|
|
||||||
|
The echo endpont listens for incoming WebSocket connections coming in to `ws://localhost:1323/echo`. When it receives messages from any WebSocket client, it responds with that same content wrapped as an OOB Swap. So, if you post the message `Hello There. General Kenobi.` the server will respond with this: `<div id="idMessage" hx-swap-oob="true">Hello There. General Kenobi.</div>`
|
||||||
|
|
||||||
|
### Heartbeat
|
||||||
|
|
||||||
|
The heartbeat endpoint `ws://localhost:1323/heartbeat`. It does not process any messages that are sent to it, but it does send messages containing random numbers to every listener at random intervals. Heartbeat message will look like this: `<div id="idMessage" hx-swap-oob="true">12345678901234567890</div>`
|
||||||
|
|
||||||
|
## Server Sent Events
|
||||||
|
|
||||||
|
This package implements a simple server that generates Server Sent Events for your test pages to read. It streams fake data from [jsonplaceholder](https://jsonplaceholder.typicode.com) to your website on a semi-regular schedule.
|
||||||
|
|
||||||
|
### JSON Event Streams
|
||||||
|
|
||||||
|
Streams random JSON records every second (or so) to your client.
|
||||||
|
|
||||||
|
* `/posts.json`
|
||||||
|
* `/comments.json`
|
||||||
|
* `/albums.json`
|
||||||
|
* `/photos.json`
|
||||||
|
* `/todos.json`
|
||||||
|
* `/users.json`
|
||||||
|
|
||||||
|
### HTML Event Streams
|
||||||
|
|
||||||
|
Streams random HTML fragments every second (or so) to your client. These streams are used by the manual htmx tests.
|
||||||
|
|
||||||
|
* `/posts.html`
|
||||||
|
* `/comments.html`
|
||||||
|
* `/albums.html`
|
||||||
|
* `/photos.html`
|
||||||
|
* `/todos.html`
|
||||||
|
* `/users.html`
|
||||||
|
|
||||||
|
### Specifying Event Types
|
||||||
|
|
||||||
|
You can add a `type=` parameter to your URLs to specify the event name(s) that you want the server to use. You can specify multiple names in a comma separated list and the server will alternate between them. If you do not specify a type, then the default message name of `message` is used.
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
It is inspired by [jsonplaceholder](https://jsonplaceholder.typicode.com) -- *"a free online REST API that you can use whenever you need some fake data."*
|
22
test/realtime/go.mod
Normal file
22
test/realtime/go.mod
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
module github.com/bigskysoftware/htmx/test/realtime
|
||||||
|
|
||||||
|
go 1.17
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/benpate/derp v0.20.0
|
||||||
|
github.com/benpate/htmlconv v0.3.0
|
||||||
|
github.com/labstack/echo/v4 v4.1.17
|
||||||
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||||
|
golang.org/x/net v0.0.0-20200822124328-c89045814202
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/labstack/gommon v0.3.0 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
|
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||||
|
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71 // indirect
|
||||||
|
golang.org/x/text v0.3.3 // indirect
|
||||||
|
)
|
@ -17,6 +17,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
|
|||||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
|
||||||
|
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@ -42,13 +44,13 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
|
|
||||||
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71 h1:X/2sJAybVknnUnV7AD2HdT6rm2p5BP6eH2j+igduWgk=
|
||||||
|
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
@ -16,6 +16,8 @@ import (
|
|||||||
"github.com/benpate/derp"
|
"github.com/benpate/derp"
|
||||||
"github.com/benpate/htmlconv"
|
"github.com/benpate/htmlconv"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"github.com/pkg/browser"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type formatFunc func(interface{}) string
|
type formatFunc func(interface{}) string
|
||||||
@ -39,9 +41,13 @@ func main() {
|
|||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
|
||||||
e.Static("/", "static")
|
e.Static("/", "static")
|
||||||
e.Static("/htmx", "../../../src")
|
e.Static("/htmx", "../../src")
|
||||||
|
|
||||||
// JSON Event Streams
|
// Web Socket Handlers
|
||||||
|
e.GET("/echo", wsEcho)
|
||||||
|
e.GET("/heartbeat", wsHeartbeat)
|
||||||
|
|
||||||
|
// SSE - JSON Event Streams
|
||||||
e.GET("/posts.json", handleStream(makeStream(data["posts"], jsonFormatFunc)))
|
e.GET("/posts.json", handleStream(makeStream(data["posts"], jsonFormatFunc)))
|
||||||
e.GET("/comments.json", handleStream(makeStream(data["comments"], jsonFormatFunc)))
|
e.GET("/comments.json", handleStream(makeStream(data["comments"], jsonFormatFunc)))
|
||||||
e.GET("/photos.json", handleStream(makeStream(data["comments"], jsonFormatFunc)))
|
e.GET("/photos.json", handleStream(makeStream(data["comments"], jsonFormatFunc)))
|
||||||
@ -49,7 +55,7 @@ func main() {
|
|||||||
e.GET("/todos.json", handleStream(makeStream(data["todos"], jsonFormatFunc)))
|
e.GET("/todos.json", handleStream(makeStream(data["todos"], jsonFormatFunc)))
|
||||||
e.GET("/users.json", handleStream(makeStream(data["users"], jsonFormatFunc)))
|
e.GET("/users.json", handleStream(makeStream(data["users"], jsonFormatFunc)))
|
||||||
|
|
||||||
// HTML Event Streams (with HTMX extension tags)
|
// SSE - HTML Event Streams (with HTMX extension tags)
|
||||||
e.GET("/posts.html", handleStream(makeStream(data["posts"], postTemplate())))
|
e.GET("/posts.html", handleStream(makeStream(data["posts"], postTemplate())))
|
||||||
e.GET("/comments.html", handleStream(makeStream(data["comments"], commentTemplate())))
|
e.GET("/comments.html", handleStream(makeStream(data["comments"], commentTemplate())))
|
||||||
e.GET("/photos.json", handleStream(makeStream(data["comments"], jsonFormatFunc)))
|
e.GET("/photos.json", handleStream(makeStream(data["comments"], jsonFormatFunc)))
|
||||||
@ -105,9 +111,72 @@ func main() {
|
|||||||
return ctx.HTML(200, content)
|
return ctx.HTML(200, content)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// On first run, open web browser in admin mode
|
||||||
|
browser.OpenURL("http://localhost/")
|
||||||
|
|
||||||
e.Logger.Fatal(e.Start(":80"))
|
e.Logger.Fatal(e.Start(":80"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*******************************************
|
||||||
|
* Web Socket Handlers
|
||||||
|
*******************************************/
|
||||||
|
|
||||||
|
func wsHeartbeat(c echo.Context) error {
|
||||||
|
|
||||||
|
handler := websocket.Handler(func(ws *websocket.Conn) {
|
||||||
|
|
||||||
|
defer ws.Close()
|
||||||
|
|
||||||
|
for i := 0; ; i = i + 1 {
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
random := rand.Int()
|
||||||
|
message := `<div id="idMessage" hx-swap-oob="true">Message ` + strconv.Itoa(i) + `: ` + strconv.Itoa(random) + `</div>`
|
||||||
|
|
||||||
|
if err := websocket.Message.Send(ws, message); err != nil {
|
||||||
|
c.Logger().Error("send", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(c.Response(), c.Request())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsEcho(c echo.Context) error {
|
||||||
|
|
||||||
|
handler := websocket.Handler(func(ws *websocket.Conn) {
|
||||||
|
|
||||||
|
defer ws.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
|
||||||
|
msg := ""
|
||||||
|
|
||||||
|
if err := websocket.Message.Receive(ws, &msg); err != nil {
|
||||||
|
c.Logger().Error("receive", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response := `<div id="idMessage" hx-swap-oob="true">` + msg + `</div>`
|
||||||
|
|
||||||
|
if err := websocket.Message.Send(ws, response); err != nil {
|
||||||
|
c.Logger().Error("send", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
handler.ServeHTTP(c.Response(), c.Request())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/*******************************************
|
||||||
|
* SSE Handlers
|
||||||
|
*******************************************/
|
||||||
|
|
||||||
func pageHandler(ctx echo.Context, page int) error {
|
func pageHandler(ctx echo.Context, page int) error {
|
||||||
|
|
||||||
pageString := strconv.Itoa(page)
|
pageString := strconv.Itoa(page)
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
52
test/realtime/static/index.html
Normal file
52
test/realtime/static/index.html
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/stylesheet.css">
|
||||||
|
<title></> htmx Realtime Test Server</title>
|
||||||
|
<script src="/htmx/htmx.js"></script>
|
||||||
|
<script src="/htmx/ext/sse.js"></script>
|
||||||
|
<script src="/htmx/ext/ws.js"></script>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/hyperscript.org"></script>
|
||||||
|
<script type="text/hyperscript">
|
||||||
|
on click(target) from <#navigation a/>
|
||||||
|
take .selected for target
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="header"></div>
|
||||||
|
<div id="navigation" hx-target="#page" hx-push-url="false">
|
||||||
|
<a href="index.html" class="selected" hx-boost="false">Introduction</a>
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<a href="" hx-get="/ws-about.html">WebSockets</a>
|
||||||
|
<a href="" hx-get="/ws-echo.html">Echo</a>
|
||||||
|
<a href="" hx-get="/ws-heartbeat.html">Heartbeat</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="group">
|
||||||
|
<a href="" hx-get="/sse-about.html">Server Sent Events</a>
|
||||||
|
<a href="" hx-get="/sse-simple.html">Simple</a>
|
||||||
|
<a href="" hx-get="/sse-multiple.html">Multiple</a>
|
||||||
|
<a href="" hx-get="/sse-multichannel.html">Multi-Channel</a>
|
||||||
|
<a href="" hx-get="/sse-triggers.html">Event Trigger</a>
|
||||||
|
<a href="" hx-get="/sse-target.html">Event Target</a>
|
||||||
|
<a href="" hx-get="/sse-settle.html">Settling</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="page">
|
||||||
|
<h1>Realtime Test Server</h1>
|
||||||
|
|
||||||
|
<h2>New Extensions</h2>
|
||||||
|
<p>As of version 1.7, we have created two new extensions <b>ws.js</b> and <b>sse.js</b> to support realtime development in htmx. All new effort on WebSockets and Server Sent Events will occur in these extensions.</p>
|
||||||
|
|
||||||
|
<h2>Old Tags Deprecated</h2>
|
||||||
|
<p>The existing <b>hx-ws</b> and <b>hx-sse</b> tags have been deprecated and will not receive any more updates. We plan to remove these two tags from the core library in htmx version 2.0.</p>
|
||||||
|
|
||||||
|
<h2>Try It For Yourself</h2>
|
||||||
|
<p>Because extensions use a different calling syntax, there are minor differences in the way that this new code is invoked. This test server includes several demos / manual tests for each extension that you can try out for yourself. Each is presented side-by-side with test cases for the original code so that you can see the difference.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
34
test/realtime/static/sse-about.html
Normal file
34
test/realtime/static/sse-about.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<h1>Server Sent Events (SSE)</h1>
|
||||||
|
|
||||||
|
<p>SSE create a lightweight, uni-directional connection from your server to a client's web browser. They are often easier to manage than WebSockets, and are built on top ofHTTP connections (making them less likely to be blocked by firewalls).</p>
|
||||||
|
<p>As of version 1.7, SSE support has been moved into a new extension, and the existing <b>hx-sse</b> tag has been deprecated. All future development will occur in the extension code, and the deprecated tag will be removed in htmx version 2.0</p>
|
||||||
|
|
||||||
|
<h3>Required Attributes</h3>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td class="bold nowrap">hx-ext</td>
|
||||||
|
<td>Make sure the SSE extension is initialized on every page or page fragment where you use SSE streams.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="bold nowrap">sse-connect</td>
|
||||||
|
<td>Connects to a SSE event stream</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="bold nowrap">sse-swap</td>
|
||||||
|
<td>Specifies the messages that a particular DOM element will listen to.</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Example Code</h3>
|
||||||
|
|
||||||
|
<pre class="code">
|
||||||
|
<body hx-ext="sse">
|
||||||
|
<div sse-connect="https://my.sse.server.com" sse-swap="message"></div>
|
||||||
|
</body>
|
||||||
|
</pre>
|
||||||
|
<h3>SSE Resources</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://en.wikipedia.org/wiki/Server-sent_events" target="_blank">Wikipedia</a></li>
|
||||||
|
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events" target="_blank">MDN Web Docs</a></li>
|
||||||
|
<li><a href="https://caniuse.com/eventsource" target="_blank">Can I Use?</a></li>
|
||||||
|
</ul>
|
@ -1,4 +1,10 @@
|
|||||||
<h1>Multi-Channel Test</h1>
|
<h1>Multi-Channel Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-multichannel.html">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-multichannel-ext.html" aria-selected="true">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<p>
|
<p>
|
||||||
This page connects to a single different Server Sent Event (SSE) stream, listening on events named "Event1", "Event2", "Event3", and "Event4".
|
This page connects to a single different Server Sent Event (SSE) stream, listening on events named "Event1", "Event2", "Event3", and "Event4".
|
25
test/realtime/static/sse-multichannel.html
Normal file
25
test/realtime/static/sse-multichannel.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<h1>Multi-Channel Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-multichannel.html" aria-selected="true">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-multichannel-ext.html">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>
|
||||||
|
This page connects to a single different Server Sent Event (SSE) stream, listening on events named "Event1", "Event2", "Event3", and "Event4".
|
||||||
|
Each separate kind of event should swap into a different container.
|
||||||
|
</p>
|
||||||
|
<h3>Example HTML</h3>
|
||||||
|
<pre class="code">
|
||||||
|
<div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||||
|
<div hx-sse="swap:Event1">Waiting for Posts in Event1 channel...</div>
|
||||||
|
</div>
|
||||||
|
</pre>
|
||||||
|
<div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||||
|
<h3>Test Cases</h3>
|
||||||
|
<div class="container" hx-sse="swap:Event1">Waiting for Posts in Event1 channel...</div>
|
||||||
|
<div class="container" hx-sse="swap:Event2">Waiting for Posts in Event2 channel...</div>
|
||||||
|
<div class="container" hx-sse="swap:Event3">Waiting for Posts in Event3 channel...</div>
|
||||||
|
<div class="container" hx-sse="swap:Event4">Waiting for Posts in Event4 channel...</div>
|
||||||
|
</div>
|
@ -1,4 +1,10 @@
|
|||||||
<h1>Multiple Events Test</h1>
|
<h1>Multiple Events Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-multiple.html">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-multiple-ext.html" aria-selected="true">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<p>
|
<p>
|
||||||
This page connects to a single Server Sent Event (SSE) streams, but listens to multiple events.
|
This page connects to a single Server Sent Event (SSE) streams, but listens to multiple events.
|
22
test/realtime/static/sse-multiple.html
Normal file
22
test/realtime/static/sse-multiple.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<h1>Multiple Events Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-multiple.html" aria-selected="true">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-multiple-ext.html">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>
|
||||||
|
This page connects to a single Server Sent Event (SSE) streams, but listens to multiple events.
|
||||||
|
</p>
|
||||||
|
<h3>Example HTML</h3>
|
||||||
|
<pre class="code">
|
||||||
|
<div hx-sse="connect:http://localhost/posts.html?types=Event1,Event2 swap:Event1 swap:Event2">Waiting for Posts...</div>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h3>Test Cases</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="container" hx-sse="connect:http://localhost/posts.html?types=Event1,Event2 swap:Event1 swap:Event2">Waiting for Posts...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,4 +1,10 @@
|
|||||||
<h1>Settling Test</h1>
|
<h1>Settling Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-settle.html">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-settle-ext.html" aria-selected="true">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<p>
|
<p>
|
||||||
This page connects to a single different Server Sent Event (SSE) stream.
|
This page connects to a single different Server Sent Event (SSE) stream.
|
||||||
@ -10,6 +16,7 @@
|
|||||||
<div sse-swap="message" hx-swap="settle:100ms">Waiting for Comments...</div>
|
<div sse-swap="message" hx-swap="settle:100ms">Waiting for Comments...</div>
|
||||||
</div>
|
</div>
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<h3>Test Cases</h3>
|
<h3>Test Cases</h3>
|
||||||
<div hx-ext="sse" sse-connect="http://localhost/comments.html">
|
<div hx-ext="sse" sse-connect="http://localhost/comments.html">
|
||||||
<div class="container" sse-swap="message" hx-swap="innerHTML settle:50ms">Waiting for Comments...</div>
|
<div class="container" sse-swap="message" hx-swap="innerHTML settle:50ms">Waiting for Comments...</div>
|
28
test/realtime/static/sse-settle.html
Normal file
28
test/realtime/static/sse-settle.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<h1>Settling Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-settle.html" aria-selected="true">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-settle-ext.html">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>
|
||||||
|
This page connects to a single different Server Sent Event (SSE) stream.
|
||||||
|
Multiple containers all listen for the same default "message" event name, but using different values for hx-swap.
|
||||||
|
</p>
|
||||||
|
<h3>Example HTML</h3>
|
||||||
|
<pre class="code">
|
||||||
|
<div hx-sse="connect:http://localhost/comments.html">
|
||||||
|
<div hx-sse="swap:message" hx-swap="settle:100ms">Waiting for Comments...</div>
|
||||||
|
</div>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h3>Test Cases</h3>
|
||||||
|
<div hx-sse="connect:http://localhost/comments.html">
|
||||||
|
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:50ms">Waiting for Comments...</div>
|
||||||
|
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:100ms">Waiting for Comments...</div>
|
||||||
|
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:150ms">Waiting for Comments...</div>
|
||||||
|
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:200ms">Waiting for Comments...</div>
|
||||||
|
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:250ms">Waiting for Comments...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,4 +1,10 @@
|
|||||||
<h1>Simple Test</h1>
|
<h1>Simple Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-simple.html">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-simple-ext.html" aria-selected="true">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<p>
|
<p>
|
||||||
This page connects to several different Server Sent Event (SSE) streams, listening on the default event name "message".
|
This page connects to several different Server Sent Event (SSE) streams, listening on the default event name "message".
|
26
test/realtime/static/sse-simple.html
Normal file
26
test/realtime/static/sse-simple.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<h1>Simple Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-simple.html" aria-selected="true">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-simple-ext.html">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>
|
||||||
|
This page connects to several different Server Sent Event (SSE) streams, listening on the default event name "message".
|
||||||
|
Each stream should populate its own container.
|
||||||
|
</p>
|
||||||
|
<h3>Example HTML</h3>
|
||||||
|
<pre class="code">
|
||||||
|
<div hx-sse="connect:http://localhost/posts.html swap:message">Waiting for Posts...</div>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<h3>Test Cases</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="container" hx-sse="connect:http://localhost/posts.html swap:message">Waiting for Posts...</div>
|
||||||
|
<div class="container" hx-sse="connect:http://localhost/comments.html swap:message">Waiting for Comments...</div>
|
||||||
|
<div class="container" hx-sse="connect:http://localhost/albums.html swap:message">Waiting for Albums...</div>
|
||||||
|
<div class="container" hx-sse="connect:http://localhost/todos.html swap:message">Waiting for ToDos...</div>
|
||||||
|
<div class="container" hx-sse="connect:http://localhost/users.html swap:message">Waiting for Users...</div>
|
||||||
|
</div>
|
@ -1,5 +1,10 @@
|
|||||||
<h1>Event Target Test</h1>
|
<h1>Event Target Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-target.html">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-target-ext.html" aria-selected="true">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<p>
|
<p>
|
||||||
This page connects to several different different Server Sent Event (SSE) stream.
|
This page connects to several different different Server Sent Event (SSE) stream.
|
12
test/realtime/static/sse-target.html
Normal file
12
test/realtime/static/sse-target.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<h1>Event Target Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-target.html" aria-selected="true">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-target-ext.html">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>
|
||||||
|
The original implementation of <b>hx-sse</b> does not use for <b>hx-target</b> attributes.
|
||||||
|
Therefore, this test is not implemented on this page.
|
||||||
|
</p>
|
@ -1,5 +1,10 @@
|
|||||||
<h1>Event Trigger Test</h1>
|
<h1>Event Trigger Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-triggers.html">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-triggers-ext.html" aria-selected="true">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<p>
|
<p>
|
||||||
This page connects to a single different Server Sent Event (SSE) streams, listening on events named "Event1", "Event2", "Event3", and "Event4".
|
This page connects to a single different Server Sent Event (SSE) streams, listening on events named "Event1", "Event2", "Event3", and "Event4".
|
26
test/realtime/static/sse-triggers.html
Normal file
26
test/realtime/static/sse-triggers.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<h1>Event Trigger Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/sse-triggers.html" aria-selected="true">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/sse-triggers-ext.html">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>
|
||||||
|
This page connects to a single different Server Sent Event (SSE) streams, listening on events named "Event1", "Event2", "Event3", and "Event4".
|
||||||
|
Each event is used as a trigger for hx-get to load a random page from the server.
|
||||||
|
</p>
|
||||||
|
<h3>Example HTML</h3>
|
||||||
|
<pre class="code">
|
||||||
|
<div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||||
|
<div hx-get="http://localhost/page/random" hx-trigger="sse:Event1">Waiting for Posts...</div>
|
||||||
|
</div>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||||
|
<h3>Test Cases</h3>
|
||||||
|
<div class="container" hx-get="http://localhost/page/random" hx-trigger="sse:Event1">Waiting for Posts in Event1 channel...</div>
|
||||||
|
<div class="container" hx-get="http://localhost/page/random" hx-trigger="sse:Event2">Waiting for Posts in Event2 channel...</div>
|
||||||
|
<div class="container" hx-get="http://localhost/page/random" hx-trigger="sse:Event3">Waiting for Posts in Event3 channel...</div>
|
||||||
|
<div class="container" hx-get="http://localhost/page/random" hx-trigger="sse:Event4">Waiting for Posts in Event4 channel...</div>
|
||||||
|
</div>
|
222
test/realtime/static/stylesheet.css
Normal file
222
test/realtime/static/stylesheet.css
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
/***************************
|
||||||
|
* GLOBAL RESET
|
||||||
|
***************************/
|
||||||
|
|
||||||
|
*{
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--white: #ffffff;
|
||||||
|
--gray05: #fafafa;
|
||||||
|
--gray10: #f4f4f4;
|
||||||
|
--gray15: #eaeaea;
|
||||||
|
--gray20: #e0e0e0;
|
||||||
|
--gray30: #c6c6c6;
|
||||||
|
--gray40: #a8a8a8;
|
||||||
|
--gray50: #8d8d8d;
|
||||||
|
--gray60: #6f6f6f;
|
||||||
|
--gray70: #525252;
|
||||||
|
--gray80: #393939;
|
||||||
|
--gray90: #262626;
|
||||||
|
--black: #000000;
|
||||||
|
|
||||||
|
--blue10: #edf5ff;
|
||||||
|
--blue20: #d0e2ff;
|
||||||
|
--blue30: #a6c8ff;
|
||||||
|
--blue40: #78a9ff;
|
||||||
|
--blue50: #4589ff;
|
||||||
|
--blue60: #0f62fe;
|
||||||
|
--blue70: #0043ce;
|
||||||
|
--blue80: #002d9c;
|
||||||
|
--blue90: #001d6c;
|
||||||
|
--blue100: #001141;
|
||||||
|
|
||||||
|
--red10: #fff1f1;
|
||||||
|
--red20: #ffd7d9;
|
||||||
|
--red30: #ffb3b8;
|
||||||
|
--red40: #ff8389;
|
||||||
|
--red50: #fa4d56;
|
||||||
|
--red60: #da1e28;
|
||||||
|
--red70: #a2191f;
|
||||||
|
--red80: #750e13;
|
||||||
|
--red90: #520408;
|
||||||
|
--red100: #2d0709;
|
||||||
|
|
||||||
|
--input-border: var(--gray30);
|
||||||
|
--input-background: var(--gray05);
|
||||||
|
--input-color: var(--gray80);
|
||||||
|
|
||||||
|
--input-border-invalid: var(--red40);
|
||||||
|
--input-background-invalid: var(--red20);
|
||||||
|
--input-color-invalid: var(--red50);
|
||||||
|
|
||||||
|
--border-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************
|
||||||
|
* CHROME AND LAYOUT
|
||||||
|
***************************/
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: white;
|
||||||
|
padding:0px;
|
||||||
|
margin:0px;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header {
|
||||||
|
width:100%;
|
||||||
|
height: 100px;
|
||||||
|
background-image:url('white_transparent.svg');
|
||||||
|
background-position:left 50px center;
|
||||||
|
background-repeat:no-repeat;
|
||||||
|
background-size: 300px;
|
||||||
|
background-color:black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navigation {
|
||||||
|
position:absolute;
|
||||||
|
width:200px;
|
||||||
|
margin-top:50px;
|
||||||
|
margin-left:20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navigation .group {
|
||||||
|
margin-top:20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navigation a:first-child {
|
||||||
|
color: black;
|
||||||
|
font-weight:500;
|
||||||
|
display:block;
|
||||||
|
text-decoration:none;
|
||||||
|
padding:10px 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navigation a {
|
||||||
|
display:block;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration:none;
|
||||||
|
padding:10px 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navigation a:hover {
|
||||||
|
background-color:#eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
#navigation a.selected {
|
||||||
|
color:white;
|
||||||
|
background-color:#3465a4;
|
||||||
|
font-weight:bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page {
|
||||||
|
margin: 50px;
|
||||||
|
padding-left:200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
border: solid 1px gray;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
background-color:#f7f7f7;
|
||||||
|
height:130px;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container.htmx-settling {
|
||||||
|
border:solid 3px red!important;
|
||||||
|
padding:8px!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/***************************
|
||||||
|
* TAB STYLES
|
||||||
|
***************************/
|
||||||
|
|
||||||
|
[role="tablist"] {
|
||||||
|
border-bottom: solid 1px var(--gray40);
|
||||||
|
margin-bottom:20px;
|
||||||
|
line-height:normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] > [role="tab"] {
|
||||||
|
cursor: pointer;
|
||||||
|
display:inline-block;
|
||||||
|
padding: 8px 16px 4px 16px;
|
||||||
|
margin:0px 2px -1px 0px;
|
||||||
|
background-color:var(--gray05);
|
||||||
|
border:solid 1px var(--gray30);
|
||||||
|
border-bottom: solid 1px var(--gray40);
|
||||||
|
border-radius: 4px 4px 0px 0px;
|
||||||
|
color: var(--gray50);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size:1.1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] > [role="tab"]:hover,
|
||||||
|
[role="tablist"] > [role="tab"]:focus {
|
||||||
|
background-color: var(--gray20);
|
||||||
|
border-color:var(--gray10);
|
||||||
|
border-bottom: solid 1px var(--gray40);
|
||||||
|
color:#666;
|
||||||
|
}
|
||||||
|
|
||||||
|
[role="tablist"] > [role="tab"][aria-selected="true"] {
|
||||||
|
border-color: var(--gray40);
|
||||||
|
border-bottom: solid 1px white;
|
||||||
|
background-color: white;
|
||||||
|
color: var(--gray100);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***************************
|
||||||
|
* OTHER UTILITIES
|
||||||
|
***************************/
|
||||||
|
|
||||||
|
|
||||||
|
pre.code {
|
||||||
|
font-family:'Courier New', Courier, monospace;
|
||||||
|
background-color: #444440;
|
||||||
|
color: #0f0;
|
||||||
|
padding:30px 5px 30px 15px;
|
||||||
|
overflow-y:scroll;
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold {
|
||||||
|
font-weight:bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding:10px 20px;
|
||||||
|
border:solid 1px #ddd;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo {
|
||||||
|
padding:10px;
|
||||||
|
margin:20px 0px;
|
||||||
|
color:white;
|
||||||
|
background-color: #999;
|
||||||
|
height:100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:visited {
|
||||||
|
color:#3465a4;
|
||||||
|
}
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
34
test/realtime/static/ws-about.html
Normal file
34
test/realtime/static/ws-about.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<h1>WebSockets</h1>
|
||||||
|
|
||||||
|
<p>WebSockets create a fast, bi-directional connection between your server and a client's web browser.</p>
|
||||||
|
<p>As of version 1.7, WebSocket support has been moved into a new extension, and the existing <b>hx-ws</b> tag has been deprecated. All future development will occur in the extension code, and the deprecated tag will be removed in htmx version 2.0</p>
|
||||||
|
|
||||||
|
<h3>Required Attributes</h3>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td class="bold nowrap">hx-ext</td>
|
||||||
|
<td>Make sure the WS extension is initialized on every page or page fragment where you use WebSockets.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="bold nowrap">ws-connect</td>
|
||||||
|
<td>Connects to a WebSocket. All received messages parsed as OOB Swaps.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="bold nowrap">ws-send</td>
|
||||||
|
<td>Marks a form that, when submitted, will have its contents serialized and sent to the connected WebSocket server</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Example Code</h3>
|
||||||
|
|
||||||
|
<pre class="code">
|
||||||
|
<body>
|
||||||
|
<div hx-ext="ws" ws-connect="https://my.websocket.server.com"></div>
|
||||||
|
</body>
|
||||||
|
</pre>
|
||||||
|
<h3>WebSocket Resources</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://en.wikipedia.org/wiki/WebSocket" target="_blank">Wikipedia</a></li>
|
||||||
|
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket" target="_blank">MDN Web Docs</a></li>
|
||||||
|
<li><a href="https://caniuse.com/websocket" target="_blank">Can I Use?</a></li>
|
||||||
|
</ul>
|
@ -1,4 +1,10 @@
|
|||||||
<h1>Echo Test</h1>
|
<h1>Echo Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/ws-echo.html">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/ws-echo-ext.html" aria-selected="true">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<p>This test lets you send and receive data to and from the WebSocket server. Every message that you send to the server will be "echoed"
|
<p>This test lets you send and receive data to and from the WebSocket server. Every message that you send to the server will be "echoed"
|
||||||
back to you in a separate message</p>
|
back to you in a separate message</p>
|
39
test/realtime/static/ws-echo.html
Normal file
39
test/realtime/static/ws-echo.html
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<h1>Echo Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/ws-echo.html" aria-selected="true">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/ws-echo-ext.html">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>This test lets you send and receive data to and from the WebSocket server. Every message that you send to the server will be "echoed"
|
||||||
|
back to you in a separate message</p>
|
||||||
|
|
||||||
|
<h3>Example HTML</h3>
|
||||||
|
|
||||||
|
<pre class="code">
|
||||||
|
<div hx-ws="connect:ws://localhost/echo">
|
||||||
|
|
||||||
|
<form hx-ws="send">
|
||||||
|
<input type="text" name="message" style="width:500px;" value="This Is The Message" />
|
||||||
|
<input type="submit"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="idMessage"></div>
|
||||||
|
</div>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<div class="container" hx-ws="connect:ws://localhost/echo">
|
||||||
|
|
||||||
|
<form hx-ws="send">
|
||||||
|
<h3>Send a Message</h3>
|
||||||
|
<div>
|
||||||
|
<input type="text" name="message" style="width:500px;" value="This Is The Message" />
|
||||||
|
<input type="submit" value="Send" class="btn primary"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<h3>Receive a Message</h3>
|
||||||
|
<div id="idMessage"></div>
|
||||||
|
</div>
|
@ -1,4 +1,10 @@
|
|||||||
<h1>Heartbeat Test</h1>
|
<h1>Heartbeat Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/ws-heartbeat.html">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/ws-heartbeat-ext.html" aria-selected="true">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h3>Description</h3>
|
<h3>Description</h3>
|
||||||
<p>This test receives messages from the WebSocket server every second.
|
<p>This test receives messages from the WebSocket server every second.
|
||||||
|
|
23
test/realtime/static/ws-heartbeat.html
Normal file
23
test/realtime/static/ws-heartbeat.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<h1>Heartbeat Test</h1>
|
||||||
|
|
||||||
|
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||||
|
<a role="tab" hx-get="/ws-heartbeat.html" aria-selected="true">Legacy Style</a>
|
||||||
|
<a role="tab" hx-get="/ws-heartbeat-ext.html">New Style</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Description</h3>
|
||||||
|
<p>This test receives messages from the WebSocket server every second.
|
||||||
|
|
||||||
|
<h3>Example HTML</h3>
|
||||||
|
|
||||||
|
<pre class="code">
|
||||||
|
<div hx-ws="connect:ws://localhost/heartbeat">
|
||||||
|
<div id="idMessage"></div>
|
||||||
|
</div>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<div class="container" hx-ws="connect:ws://localhost/heartbeat">
|
||||||
|
<h3>WebSocket Messages</h3>
|
||||||
|
<p>Each message just contains a random number generated by the server</p>
|
||||||
|
<div id="idMessage">Waiting...</div>
|
||||||
|
</div>
|
22
test/realtime/static/ws-reconnect.html
Normal file
22
test/realtime/static/ws-reconnect.html
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="../../src/htmx.js"></script>
|
||||||
|
<title>WebSockets Test</title>
|
||||||
|
</head>
|
||||||
|
<body hx-ws="connect:wss://echo.websocket.org">
|
||||||
|
|
||||||
|
<form hx-ws="send">
|
||||||
|
Send Message to Echo Server...<br>
|
||||||
|
<textarea name="message" style="width:500px; height:300px;"><div id="idMessage">This Is The Message<div></textarea>
|
||||||
|
<br/><input type="submit"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Receive doesn't work with this `echo` server because of differences in the way HTMX formats
|
||||||
|
`send` messages vs. what it expects for replies. It has no bearing on the reconnect test.
|
||||||
|
|
||||||
|
<br><hr><br>
|
||||||
|
<div id="idMessage"></div>
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -1,43 +0,0 @@
|
|||||||
# Server Sent Events - Test Server
|
|
||||||
|
|
||||||
This package implements a simple server that generates Server Sent Events for your test pages to read. It streams fake data from [jsonplaceholder](https://jsonplaceholder.typicode.com) to your website on a semi-regular schedule.
|
|
||||||
|
|
||||||
## How to Use This Server
|
|
||||||
|
|
||||||
1. If you do not already have Go (version 1.17 or higher) installed on your machine, you can download an installation for your machine from [the Go website](https://golang.org)
|
|
||||||
|
|
||||||
2. Open up a terminal window and navigate to this directory. Start up the WebSocket server by typing `go run server.go`
|
|
||||||
|
|
||||||
3. Open your web browser to [http://localhost](http://localhost) to run the manual tests. Huzzah!
|
|
||||||
|
|
||||||
## JSON Event Streams
|
|
||||||
|
|
||||||
Streams random JSON records every second (or so) to your client.
|
|
||||||
|
|
||||||
* `/posts.json`
|
|
||||||
* `/comments.json`
|
|
||||||
* `/albums.json`
|
|
||||||
* `/photos.json`
|
|
||||||
* `/todos.json`
|
|
||||||
* `/users.json`
|
|
||||||
|
|
||||||
## HTML Event Streams
|
|
||||||
|
|
||||||
Streams random HTML fragments every second (or so) to your client. These streams are used by the manual htmx tests.
|
|
||||||
|
|
||||||
* `/posts.html`
|
|
||||||
* `/comments.html`
|
|
||||||
* `/albums.html`
|
|
||||||
* `/photos.html`
|
|
||||||
* `/todos.html`
|
|
||||||
* `/users.html`
|
|
||||||
|
|
||||||
## Specifying Event Types
|
|
||||||
|
|
||||||
You can add a `type=` parameter to your URLs to specify the event name(s) that you want the server to use. You can specify multiple names in a comma separated list and the server will alternate between them. If you do not specify a type, then the default message name of `message` is used.
|
|
||||||
|
|
||||||
## About
|
|
||||||
|
|
||||||
This server is also published independently at [https://github.com/benpate/sseplaceholder]
|
|
||||||
|
|
||||||
It is inspired by [jsonplaceholder](https://jsonplaceholder.typicode.com) -- *"a free online REST API that you can use whenever you need some fake data."*
|
|
@ -1,9 +0,0 @@
|
|||||||
module github.com/benpate/sseplaceholder
|
|
||||||
|
|
||||||
go 1.16
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/benpate/derp v0.20.0
|
|
||||||
github.com/benpate/htmlconv v0.3.0
|
|
||||||
github.com/labstack/echo/v4 v4.1.17
|
|
||||||
)
|
|
@ -1,63 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="/stylesheet.css">
|
|
||||||
<title></> htmx SSE Test Server</title>
|
|
||||||
<script src="/htmx/htmx.js"></script>
|
|
||||||
<script src="/htmx/ext/sse.js"></script>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/hyperscript.org@0.8.3"></script>
|
|
||||||
<script type="text/hyperscript">
|
|
||||||
on click(target) from <#navigation a/>
|
|
||||||
take .selected for target
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div id="header"></div>
|
|
||||||
<div id="navigation" hx-target="#page" hx-push-url="false">
|
|
||||||
<a href="index.html" class="selected" hx-boost="false">About</a>
|
|
||||||
<a href="" hx-get="sse-simple.html">Simple</a>
|
|
||||||
<a href="" hx-get="sse-multiple.html">Multiple</a>
|
|
||||||
<a href="" hx-get="sse-multichannel.html">Multi-Channel</a>
|
|
||||||
<a href="" hx-get="sse-triggers.html">Event Trigger</a>
|
|
||||||
<a href="" hx-get="sse-target.html">Event Target</a>
|
|
||||||
<a href="" hx-get="sse-settle.html">Settling</a>
|
|
||||||
</div>
|
|
||||||
<div id="page">
|
|
||||||
<h1>Server Sent Events (SSE) Extension Tests</h1>
|
|
||||||
|
|
||||||
<p>As of version 1.7, SSE support has been moved out of the core htmx library and into an extension. This server runs a test suite for the htmx SSE extension.</p>
|
|
||||||
<p>This extension listens for real-time events that are pushed from the server and can swap them into your htmx webpage.</p>
|
|
||||||
|
|
||||||
<h3>Required Attributes</h3>
|
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<td class="bold nowrap">hx-ext</td>
|
|
||||||
<td>Make sure the SSE extension is initialized on every page or page fragment where you use SSE streams.</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="bold nowrap">sse-connect</td>
|
|
||||||
<td>Connects to a SSE event stream</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="bold nowrap">sse-swap</td>
|
|
||||||
<td>Specifies the messages that a particular DOM element will listen to.</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<h3>Example Code</h3>
|
|
||||||
|
|
||||||
<pre class="code">
|
|
||||||
<body hx-ext="sse">
|
|
||||||
<div sse-connect="https://my.sse.server.com" sse-swap="message"></div>
|
|
||||||
</body>
|
|
||||||
</pre>
|
|
||||||
<h3>SSE Resources</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://en.wikipedia.org/wiki/Server-sent_events">Wikipedia</a></li>
|
|
||||||
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events">MDN Web Docs</a></li>
|
|
||||||
<li><a href="https://caniuse.com/eventsource">Can I Use?</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
@ -1,102 +0,0 @@
|
|||||||
*{
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: white;
|
|
||||||
padding:0px;
|
|
||||||
margin:0px;
|
|
||||||
width:100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header {
|
|
||||||
width:100%;
|
|
||||||
height: 100px;
|
|
||||||
background-image:url('white_transparent.svg');
|
|
||||||
background-position:left 50px center;
|
|
||||||
background-repeat:no-repeat;
|
|
||||||
background-size: 300px;
|
|
||||||
background-color:black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#navigation {
|
|
||||||
position:absolute;
|
|
||||||
width:150px;
|
|
||||||
margin-top:50px;
|
|
||||||
margin-left:20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#navigation > a {
|
|
||||||
display:block;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration:none;
|
|
||||||
padding:10px 20px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
#navigation > a:hover {
|
|
||||||
background-color:#eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
#navigation > a.selected {
|
|
||||||
font-weight:bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page {
|
|
||||||
margin: 50px;
|
|
||||||
padding-left:150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: 10px;
|
|
||||||
border: solid 1px gray;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
background-color:#f7f7f7;
|
|
||||||
height:130px;
|
|
||||||
overflow:hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container.htmx-settling {
|
|
||||||
border:solid 3px red!important;
|
|
||||||
padding:8px!important;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre.code {
|
|
||||||
font-family:'Courier New', Courier, monospace;
|
|
||||||
background-color: #444440;
|
|
||||||
color: #0f0;
|
|
||||||
padding:30px 5px 30px 15px;
|
|
||||||
overflow-y:scroll;
|
|
||||||
display:block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bold {
|
|
||||||
font-weight:bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nowrap {
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
td {
|
|
||||||
padding:10px 20px;
|
|
||||||
border:solid 1px #ddd;
|
|
||||||
vertical-align: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
.demo {
|
|
||||||
padding:10px;
|
|
||||||
margin:20px 0px;
|
|
||||||
color:white;
|
|
||||||
background-color: #999;
|
|
||||||
height:100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a, a:visited {
|
|
||||||
color:#3465a4;
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
# WebSocket - Test Server
|
# Htmx Realtime Test Server
|
||||||
|
|
||||||
This package implements a test-suite WebSocket server for testing htmx.
|
This package implements a realtime server for testing WebSockets and Server Sent Events (SSE) in htmx.
|
||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
|
@ -3,9 +3,9 @@ layout: layout.njk
|
|||||||
title: </> htmx - hx-sse
|
title: </> htmx - hx-sse
|
||||||
---
|
---
|
||||||
|
|
||||||
## `hx-sse` *HAS BEEN MIGRATED TO AN EXTENSION*
|
## `hx-sse` *HAS BEEN DEPRECATED*
|
||||||
|
|
||||||
**If you are using htmx version 1.6.1 or greater, please visit the [SSE extension page](../extensions/server-sent-events) to learn about the new implementation of Server Sent Events as an extension.
|
**This tag will be removed in htmx version 2.0. If you are using htmx version 1.7 or greater, please visit the [SSE extension page](../extensions/server-sent-events) to learn about the new implementation of Server Sent Events as an extension.
|
||||||
|
|
||||||
## This Reference Applies To Version 1.6 And Below
|
## This Reference Applies To Version 1.6 And Below
|
||||||
|
|
||||||
@ -15,7 +15,6 @@ The `hx-sse` allows you to work with [Server Sent Event](https://developer.mozil
|
|||||||
* `connect:<url>` - A URL to establish an `EventSource` against
|
* `connect:<url>` - A URL to establish an `EventSource` against
|
||||||
* `swap:<eventName>` - Swap SSE message content into a DOM node on matching event names
|
* `swap:<eventName>` - Swap SSE message content into a DOM node on matching event names
|
||||||
|
|
||||||
|
|
||||||
### Swap Message Content
|
### Swap Message Content
|
||||||
|
|
||||||
When an SSE connection has been established (using the `connect` keyword) the contents of SSE messages can be swapped into the DOM using the `swap` keyword. This can be done on the element that creates the SSE connection, or any child element of it. Multiple elements can use `swap` to listen for Server Sent Events.
|
When an SSE connection has been established (using the `connect` keyword) the contents of SSE messages can be swapped into the DOM using the `swap` keyword. This can be done on the element that creates the SSE connection, or any child element of it. Multiple elements can use `swap` to listen for Server Sent Events.
|
||||||
@ -87,6 +86,10 @@ data: <div>Content to swap into your HTML page.</div>
|
|||||||
<div hx-sse="connect:/server-url swap:message"></div>
|
<div hx-sse="connect:/server-url swap:message"></div>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Test SSE Server
|
||||||
|
|
||||||
|
Htmx includes an SSE test server with many more examples of how to use Server Sent Events. Download the htmx source code from github and navigate to /test/servers/sse to experiment.
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
* `hx-sse` is not inherited
|
* `hx-sse` is not inherited
|
||||||
|
@ -3,9 +3,9 @@ layout: layout.njk
|
|||||||
title: </> htmx - hx-ws
|
title: </> htmx - hx-ws
|
||||||
---
|
---
|
||||||
|
|
||||||
## `hx-ws` *HAS BEEN MIGRATED TO AN EXTENSION*
|
## `hx-ws` *HAS BEEN DEPRECATED*
|
||||||
|
|
||||||
**If you are using htmx version 1.6.1 or greater, please visit the [WebSockets extension page](../extensions/web-sockets) to learn about the new implementation of Web Sockets as an extension.
|
**This tag will be removed in htmx version 2.0. If you are using htmx version 1.7 or greater, please visit the [WebSockets extension page](../extensions/web-sockets) to learn about the new implementation of Web Sockets as an extension.
|
||||||
|
|
||||||
## This Reference Applies To Version 1.6 And Below
|
## This Reference Applies To Version 1.6 And Below
|
||||||
|
|
||||||
@ -45,6 +45,11 @@ The default reconnection interval is implemented with the full-jitter exponentia
|
|||||||
Own implementations can be provided by setting `htmx.config.wsReconnectDelay` to a function with
|
Own implementations can be provided by setting `htmx.config.wsReconnectDelay` to a function with
|
||||||
`retryCount` as its only parameter.
|
`retryCount` as its only parameter.
|
||||||
|
|
||||||
|
|
||||||
|
### Test Web Sockets Server
|
||||||
|
|
||||||
|
Htmx includes a WebSockets test server with many more examples of how to use Server Sent Events. Download the htmx source code from github and navigate to /test/servers/ws to experiment.
|
||||||
|
|
||||||
### Notes
|
### Notes
|
||||||
|
|
||||||
* `hx-ws` is not inherited
|
* `hx-ws` is not inherited
|
||||||
|
Loading…
x
Reference in New Issue
Block a user