Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
carson 2022-02-16 19:21:03 -07:00
commit faec23002c
43 changed files with 1365 additions and 251 deletions

165
src/ext/loading-states.js Normal file
View File

@ -0,0 +1,165 @@
;(function () {
let loadingStatesUndoQueue = []
function loadingStateContainer(target) {
return htmx.closest(target, '[data-loading-states]') || document.body
}
function mayProcessUndoCallback(target, callback) {
if (document.body.contains(target)) {
callback()
}
}
function mayProcessLoadingStateByPath(elt, requestPath) {
const pathElt = htmx.closest(elt, '[data-loading-path]')
if (!pathElt) {
return true
}
return pathElt.getAttribute('data-loading-path') === requestPath
}
function queueLoadingState(sourceElt, targetElt, doCallback, undoCallback) {
const delayElt = htmx.closest(sourceElt, '[data-loading-delay]')
if (delayElt) {
const delayInMilliseconds =
delayElt.getAttribute('data-loading-delay') || 200
const timeout = setTimeout(() => {
doCallback()
loadingStatesUndoQueue.push(() => {
mayProcessUndoCallback(targetElt, () => undoCallback())
})
}, delayInMilliseconds)
loadingStatesUndoQueue.push(() => {
mayProcessUndoCallback(targetElt, () => clearTimeout(timeout))
})
} else {
doCallback()
loadingStatesUndoQueue.push(() => {
mayProcessUndoCallback(targetElt, () => undoCallback())
})
}
}
function getLoadingStateElts(loadingScope, type, path) {
return Array.from(htmx.findAll(loadingScope, `[${type}]`)).filter(
(elt) => mayProcessLoadingStateByPath(elt, path)
)
}
function getLoadingTarget(elt) {
if (elt.getAttribute('data-loading-target')) {
return Array.from(
htmx.findAll(elt.getAttribute('data-loading-target'))
)
}
return [elt]
}
htmx.defineExtension('loading-states', {
onEvent: function (name, evt) {
if (name === 'htmx:beforeRequest') {
const container = loadingStateContainer(evt.target)
const loadingStateTypes = [
'data-loading',
'data-loading-class',
'data-loading-class-remove',
'data-loading-disable',
]
let loadingStateEltsByType = {}
loadingStateTypes.forEach((type) => {
loadingStateEltsByType[type] = getLoadingStateElts(
container,
type,
evt.detail.pathInfo.path
)
})
loadingStateEltsByType['data-loading'].forEach((sourceElt) => {
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() =>
(targetElt.style.display =
sourceElt.getAttribute('data-loading') ||
'inline-block'),
() => (targetElt.style.display = 'none')
)
})
})
loadingStateEltsByType['data-loading-class'].forEach(
(sourceElt) => {
const classNames = sourceElt
.getAttribute('data-loading-class')
.split(' ')
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() =>
classNames.forEach((className) =>
targetElt.classList.add(className)
),
() =>
classNames.forEach((className) =>
targetElt.classList.remove(className)
)
)
})
}
)
loadingStateEltsByType['data-loading-class-remove'].forEach(
(sourceElt) => {
const classNames = sourceElt
.getAttribute('data-loading-class-remove')
.split(' ')
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() =>
classNames.forEach((className) =>
targetElt.classList.remove(className)
),
() =>
classNames.forEach((className) =>
targetElt.classList.add(className)
)
)
})
}
)
loadingStateEltsByType['data-loading-disable'].forEach(
(sourceElt) => {
getLoadingTarget(sourceElt).forEach((targetElt) => {
queueLoadingState(
sourceElt,
targetElt,
() => (targetElt.disabled = true),
() => (targetElt.disabled = false)
)
})
}
)
}
if (name === 'htmx:afterOnLoad') {
while (loadingStatesUndoQueue.length > 0) {
loadingStatesUndoQueue.shift()()
}
}
},
})
})()

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")
@ -1058,6 +1072,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");
@ -1331,6 +1347,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) {
@ -1356,7 +1585,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") {
@ -1431,7 +1662,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 [];
@ -1482,6 +1714,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

@ -22,8 +22,51 @@ The `queue` modifier can take an additional argument indicating exactly how to q
* `queue last` - queue the last request to show up while a request is in flight
* `queue all` - queue all requests that show up while a request is in flight
TODO examples from alejandros
### Notes
* `hx-sync` is inherited and can be placed on a parent element
This example resolves a race condition between a form's submit request and an individual input's validation request. Normally, without using `hx-sync`, filling out the input and immediately submitting the form triggers two parallel requests to `/validate` and `/store`. Using `hx-sync="closest form:abort"` on the input will watch for requests on the form and abort the input's request if a form request is present or starts while the input request is in flight.
```html
<form hx-post="/store">
<input id="title" name="title" type="text"
hx-post="/validate"
hx-trigger="change"
hx-sync="closest form:abort"
>
<button type="submit">Submit</button>
</form>
```
If you'd rather prioritize the validation request over the submit request, you can use the `drop` strategy. This example will prioritize the validation request over the submit request so that if a validation request is in flight, the form cannot be submitted.
```html
<form hx-post="/store">
<input id="title" name="title" type="text"
hx-post="/validate"
hx-trigger="change"
hx-sync="closest form:drop"
>
<button type="submit">Submit</button>
</form>
```
When dealing with forms that contain many inputs, you can prioritize the submit request over all input validation requests using the hx-sync `replace` stragegy on the form tag. This will cancel any in-flight validation requests and issue only the `hx-post="/store"` request. If you'd rather abort the submit request and prioritize any existing validation requests you can use the `hx-sync="this:abort"` strategy on the form tag
```html
<form hx-post="/store" hx-sync="this:replace">
<input id="title" name="title" type="text" hx-post="/validate" hx-trigger="change" />
<button type="submit">Submit</button>
</form>
```
When implementing active search functionality the hx-trigger attribute's `delay` modifier can be used to debounce the user's input and avoid making multiple requests while the user types. However, once a request is made, if the user begins typing again a new request will begin even if the previous one has not finished processing. This example will cancel any in-flight requests and use only the last request. In cases where the search input is contained within the target, then using `hx-sync` like this also helps reduce the chances that the input will be replaced while the user is still typing.
```html
<input type="search"
hx-get="/search"
hx-trigger="keyup changed delay:500ms, search"
hx-target="#search-results"
hx-sync="this:replace">
```

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

View File

@ -64,10 +64,9 @@ against `htmx` in each distribution
| [`event-header`](/extensions/event-header) | includes a JSON serialized version of the triggering event, if any
| [`include-vals`](/extensions/include-vals) | allows you to include additional values in a request
| [`json-enc`](/extensions/json-enc) | use JSON encoding in the body of requests, rather than the default `x-www-form-urlencoded`
| [`loading-states`](/extensions/loading-states) | allows you to disable inputs, add and remove CSS classes to any element while a request is in-flight.
| [`method-override`](/extensions/method-override) | use the `X-HTTP-Method-Override` header for non-`GET` and `POST` requests
| [`morphdom-swap`](/extensions/morphdom-swap) | an extension for using the [morphdom](https://github.com/patrick-steele-idem/morphdom) library as the swapping mechanism in htmx.
| [`client-side-templates`](/extensions/client-side-templates) | support for client side template processing of JSON responses
| [`debug`](/extensions/debug) | an extension for debugging of a particular element using htmx
| [`path-deps`](/extensions/path-deps) | an extension for expressing path-based dependencies [similar to intercoolerjs](http://intercoolerjs.org/docs.html#dependencies)
| [`preload`](/extensions/preload) | preloads selected `href` and `hx-get` targets based on rules you control.
| [`remove-me`](/extensions/remove-me) | allows you to remove an element after a given amount of time

View File

@ -0,0 +1,129 @@
---
layout: layout.njk
title: </> htmx - high power tools for html
---
## The `loading-states` Extension
This extension allows you to easily manage loading states while a request is in flight, including disabling elements, and adding and removing CSS classes.
### Usage
Add the `hx-ext="loading-states"` attribute to the body tag or to any parent element containing your htmx attributes.
Add the following class to your stylesheet to make sure elements are hidden by default:
```css
[data-loading] {
display: none;
}
```
### Supported attributes
- `data-loading`
Shows the element. The default style is `inline-block`, but it's possible to use any display style by specifying it in the attribute value.
```html
<div data-loading>loading</div>
<div data-loading="block">loading</div>
<div data-loading="flex">loading</div>
```
- `data-loading-class`
Adds, then removes, CSS classes to the element:
```html
<div class="transition-all ease-in-out duration-600" data-loading-class="bg-gray-100 opacity-80">
...
</div>
```
- `data-loading-class-remove`
Removes, then adds back, CSS classes from the element.
```html
<div class="p-8 bg-gray-100 transition-all ease-in-out duration-600" data-loading-class-remove="bg-gray-100">
...
</div>
```
- `data-loading-disable`
Disables an element for the duration of the request.
```html
<button data-loading-disable>Submit</button>
```
- `data-loading-delay`
Some actions may update quickly and showing a loading state in these cases may be more of a distraction. This attribute ensures that the loading state changes are applied only after 200ms if the request is not finished. The default delay can be modified through the attribute value and expressed in milliseconds:
```html
<button type="submit" data-loading-disable data-loading-delay="1000">Submit</button>
```
You can place the `data-loading-delay` attribute directly on the element you want to disable, or in any parent element.
- `data-loading-target`
Allows setting a different target to apply the loading states. The attribute value can be any valid CSS selector. The example below disables the submit button and shows the loading state when the form is submitted.
```html
<form hx-post="/save"
data-loading-target="#loading"
data-loading-class-remove="hidden">
<button type="submit" data-loading-disable>Submit</button>
</form>
<div id="loading" class="hidden">Loading ...</div>
```
- `data-loading-path`
Allows filtering the processing of loading states only for specific requests based on the request path.
```html
<form hx-post="/save">
<button type="submit" data-loading-disable data-loading-path="/save">Submit</button>
</form>
```
You can place the `data-loading-path` attribute directly on the loading state element, or in any parent element.
```html
<form hx-post="/save" data-loading-path="/save">
<button type="submit" data-loading-disable>Submit</button>
</form>
```
- `data-loading-states`
This attribute is optional and it allows defining a scope for the loading states so only elements within that scope are processed.
```html
<div data-loading-states>
<div hx-get=""></div>
<div data-loading>loading</div>
</div>
<div data-loading-states>
<div hx-get=""></div>
<div data-loading>loading</div>
</div>
<form data-loading-states hx-post="">
<div data-loading>loading</div>
</form>
```
#### Source
<https://unpkg.com/htmx.org/dist/ext/loading-states.js>