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:
Ben Pate 2022-02-12 11:11:30 -07:00 committed by GitHub
parent eb1367fb11
commit 546e346e98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1025 additions and 247 deletions

View File

@ -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");
} }
} }

View File

@ -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>

View File

@ -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
View 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
View 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
)

View File

@ -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=

View File

@ -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)

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@ -0,0 +1,52 @@
<html>
<head>
<link rel="stylesheet" href="/stylesheet.css">
<title>&lt;/&gt; 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>

View 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">
&lt;body hx-ext="sse"&gt;
&lt;div sse-connect="https://my.sse.server.com" sse-swap="message"&gt;&lt;/div&gt;
&lt;/body&gt;
</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>

View File

@ -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".

View 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">
&lt;div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4"&gt;
&lt;div hx-sse="swap:Event1"&gt;Waiting for Posts in Event1 channel...&lt;/div&gt;
&lt;/div&gt;
</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>

View File

@ -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.

View 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">
&lt;div hx-sse="connect:http://localhost/posts.html?types=Event1,Event2 swap:Event1 swap:Event2"&gt;Waiting for Posts...&lt;/div&gt;
</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>

View File

@ -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 @@
&lt;div sse-swap="message" hx-swap="settle:100ms"&gt;Waiting for Comments...&lt;/div&gt; &lt;div sse-swap="message" hx-swap="settle:100ms"&gt;Waiting for Comments...&lt;/div&gt;
&lt;/div&gt; &lt;/div&gt;
</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>

View 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">
&lt;div hx-sse="connect:http://localhost/comments.html"&gt;
&lt;div hx-sse="swap:message" hx-swap="settle:100ms"&gt;Waiting for Comments...&lt;/div&gt;
&lt;/div&gt;
</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>

View File

@ -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".

View 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">
&lt;div hx-sse="connect:http://localhost/posts.html swap:message"&gt;Waiting for Posts...&lt;/div&gt;
</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>

View File

@ -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.

View 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>

View File

@ -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".

View 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">
&lt;div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4"&gt;
&lt;div hx-get="http://localhost/page/random" hx-trigger="sse:Event1"&gt;Waiting for Posts...&lt;/div&gt;
&lt;/div&gt;
</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>

View 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;
}

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View 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">
&lt;body&gt;
&lt;div hx-ext="ws" ws-connect="https://my.websocket.server.com"&gt;&lt;/div&gt;
&lt;/body&gt;
</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>

View File

@ -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>

View 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">
&lt;div hx-ws="connect:ws://localhost/echo"&gt;
&lt;form hx-ws="send"&gt;
&lt;input type="text" name="message" style="width:500px;" value="This Is The Message" /&gt;
&lt;input type="submit"/&gt;
&lt;/form&gt;
&lt;div id="idMessage"&gt;&lt;/div&gt;
&lt;/div&gt;
</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>

View File

@ -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.

View 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">
&lt;div hx-ws="connect:ws://localhost/heartbeat"&gt;
&lt;div id="idMessage"&gt;&lt;/div&gt;
&lt;/div&gt;
</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>

View 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;">&lt;div id="idMessage"&gt;This Is The Message&lt;div&gt;</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>

View File

@ -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."*

View File

@ -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
)

View File

@ -1,63 +0,0 @@
<html>
<head>
<link rel="stylesheet" href="/stylesheet.css">
<title>&lt;/&gt; 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">
&lt;body hx-ext="sse"&gt;
&lt;div sse-connect="https://my.sse.server.com" sse-swap="message"&gt;&lt;/div&gt;
&lt;/body&gt;
</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>

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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