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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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