mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-10-02 07:21:05 +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"],
|
||||
withCredentials:false,
|
||||
timeout:0,
|
||||
wsReconnectDelay: 'full-jitter',
|
||||
disableSelector: "[hx-disable], [data-hx-disable]",
|
||||
useTemplateFragments: false,
|
||||
scrollBehavior: 'smooth',
|
||||
},
|
||||
parseInterval:parseInterval,
|
||||
_:internalEval,
|
||||
createEventSource: function(url){
|
||||
return new EventSource(url, {withCredentials:true})
|
||||
},
|
||||
createWebSocket: function(url){
|
||||
return new WebSocket(url, []);
|
||||
},
|
||||
version: "1.7.0"
|
||||
};
|
||||
|
||||
@ -562,6 +569,7 @@ return (function () {
|
||||
//====================================================================
|
||||
// Node processing
|
||||
//====================================================================
|
||||
|
||||
var DUMMY_ELT = getDocument().createElement("output"); // dummy element for bad selectors
|
||||
function findAttributeTargets(elt, attrName) {
|
||||
var attrTarget = getClosestAttributeValue(elt, attrName);
|
||||
@ -760,6 +768,12 @@ return (function () {
|
||||
|
||||
function cleanUpElement(element) {
|
||||
var internalData = getInternalData(element);
|
||||
if (internalData.webSocket) {
|
||||
internalData.webSocket.close();
|
||||
}
|
||||
if (internalData.sseEventSource) {
|
||||
internalData.sseEventSource.close();
|
||||
}
|
||||
|
||||
triggerEvent(element, "htmx:beforeCleanupElement")
|
||||
|
||||
@ -1066,6 +1080,8 @@ return (function () {
|
||||
every.eventFilter = eventFilter;
|
||||
}
|
||||
triggerSpecs.push(every);
|
||||
} else if (trigger.indexOf("sse:") === 0) {
|
||||
triggerSpecs.push({trigger: 'sse', sseEvent: trigger.substr(4)});
|
||||
} else {
|
||||
var triggerSpec = {trigger: trigger};
|
||||
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) {
|
||||
@ -1364,7 +1593,9 @@ return (function () {
|
||||
nodeData.path = path;
|
||||
nodeData.verb = verb;
|
||||
triggerSpecs.forEach(function(triggerSpec) {
|
||||
if (triggerSpec.trigger === "revealed") {
|
||||
if (triggerSpec.sseEvent) {
|
||||
processSSETrigger(elt, verb, path, triggerSpec.sseEvent);
|
||||
} else if (triggerSpec.trigger === "revealed") {
|
||||
initScrollHandler();
|
||||
maybeReveal(elt);
|
||||
} else if (triggerSpec.trigger === "intersect") {
|
||||
@ -1439,7 +1670,8 @@ return (function () {
|
||||
function findElementsToProcess(elt) {
|
||||
if (elt.querySelectorAll) {
|
||||
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;
|
||||
} else {
|
||||
return [];
|
||||
@ -1490,6 +1722,15 @@ return (function () {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
@ -126,9 +126,6 @@
|
||||
<script src="../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 -->
|
||||
<script src="core/events.js"></script>
|
||||
|
||||
|
@ -24,19 +24,6 @@
|
||||
<li><a href="scroll-test-targets.html">Targets</a></li>
|
||||
</ul>
|
||||
</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
|
||||
<ul>
|
||||
<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.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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-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-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-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.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
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=
|
||||
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/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
@ -16,6 +16,8 @@ import (
|
||||
"github.com/benpate/derp"
|
||||
"github.com/benpate/htmlconv"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pkg/browser"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
type formatFunc func(interface{}) string
|
||||
@ -39,9 +41,13 @@ func main() {
|
||||
e := echo.New()
|
||||
|
||||
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("/comments.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("/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("/comments.html", handleStream(makeStream(data["comments"], commentTemplate())))
|
||||
e.GET("/photos.json", handleStream(makeStream(data["comments"], jsonFormatFunc)))
|
||||
@ -105,9 +111,72 @@ func main() {
|
||||
return ctx.HTML(200, content)
|
||||
})
|
||||
|
||||
// On first run, open web browser in admin mode
|
||||
browser.OpenURL("http://localhost/")
|
||||
|
||||
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 {
|
||||
|
||||
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>
|
||||
|
||||
<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>
|
||||
<p>
|
||||
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>
|
||||
|
||||
<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>
|
||||
<p>
|
||||
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>
|
||||
|
||||
<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>
|
||||
<p>
|
||||
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>
|
||||
</pre>
|
||||
|
||||
<h3>Test Cases</h3>
|
||||
<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>
|
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>
|
||||
|
||||
<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>
|
||||
<p>
|
||||
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>
|
||||
|
||||
<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>
|
||||
<p>
|
||||
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>
|
||||
|
||||
<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>
|
||||
<p>
|
||||
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>
|
||||
|
||||
<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>
|
||||
<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>
|
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>
|
||||
|
||||
<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>
|
||||
<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
|
||||
|
||||
|
@ -3,9 +3,9 @@ layout: layout.njk
|
||||
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
|
||||
|
||||
@ -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
|
||||
* `swap:<eventName>` - Swap SSE message content into a DOM node on matching event names
|
||||
|
||||
|
||||
### 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.
|
||||
@ -87,6 +86,10 @@ data: <div>Content to swap into your HTML page.</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
|
||||
|
||||
* `hx-sse` is not inherited
|
||||
|
@ -3,9 +3,9 @@ layout: layout.njk
|
||||
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
|
||||
|
||||
@ -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
|
||||
`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
|
||||
|
||||
* `hx-ws` is not inherited
|
||||
|
Loading…
x
Reference in New Issue
Block a user