mirror of
https://github.com/bigskysoftware/htmx.git
synced 2025-09-29 22:11:22 +00:00
Remove the last of the go code (#1890)
Update the website tests with the latest tests on master, deleting the mirrored go code in the process.
This commit is contained in:
parent
a66a98b9ed
commit
d26fe4709d
@ -28,15 +28,27 @@ htmx.defineExtension('client-side-templates', {
|
||||
var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
|
||||
if (handlebarsTemplate) {
|
||||
var data = JSON.parse(text);
|
||||
var templateName = handlebarsTemplate.getAttribute('handlebars-template');
|
||||
return Handlebars.partials[templateName](data);
|
||||
var templateId = handlebarsTemplate.getAttribute('handlebars-template');
|
||||
var templateElement = htmx.find('#' + templateId).innerHTML;
|
||||
var renderTemplate = Handlebars.compile(templateElement);
|
||||
if (renderTemplate) {
|
||||
return renderTemplate(data);
|
||||
} else {
|
||||
throw "Unknown handlebars template: " + templateId;
|
||||
}
|
||||
}
|
||||
|
||||
var handlebarsArrayTemplate = htmx.closest(elt, "[handlebars-array-template]");
|
||||
if (handlebarsArrayTemplate) {
|
||||
var data = JSON.parse(text);
|
||||
var templateName = handlebarsArrayTemplate.getAttribute('handlebars-array-template');
|
||||
return Handlebars.partials[templateName]({"data": data});
|
||||
var templateId = handlebarsArrayTemplate.getAttribute('handlebars-array-template');
|
||||
var templateElement = htmx.find('#' + templateId).innerHTML;
|
||||
var renderTemplate = Handlebars.compile(templateElement);
|
||||
if (renderTemplate) {
|
||||
return renderTemplate(data);
|
||||
} else {
|
||||
throw "Unknown handlebars template: " + templateId;
|
||||
}
|
||||
}
|
||||
|
||||
var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
|
||||
@ -50,7 +62,7 @@ htmx.defineExtension('client-side-templates', {
|
||||
return nunjucks.render(templateName, data);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var xsltTemplate = htmx.closest(elt, "[xslt-template]");
|
||||
if (xsltTemplate) {
|
||||
var templateId = xsltTemplate.getAttribute('xslt-template');
|
||||
|
@ -29,16 +29,16 @@ describe("client-side-templates extension", function() {
|
||||
it('works on basic handlebars template', function () {
|
||||
this.server.respondWith("GET", "/test", '{"foo":"bar"}');
|
||||
var btn = make('<button hx-get="/test" hx-ext="client-side-templates" handlebars-template="hb1">Click Me!</button>')
|
||||
Handlebars.partials["hb1"] = Handlebars.compile("*{{foo}}*");
|
||||
make('<script id="hb1" type="text/x-handlebars-template">*{{foo}}*</script>')
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("*bar*");
|
||||
});
|
||||
|
||||
it('works on handlebars array template', function () {
|
||||
this.server.respondWith("GET", "/test", '{"foo":"bar"}');
|
||||
this.server.respondWith("GET", "/test", '[{"foo":"bar"}]');
|
||||
var btn = make('<button hx-get="/test" hx-ext="client-side-templates" handlebars-array-template="hb1">Click Me!</button>')
|
||||
Handlebars.partials["hb1"] = Handlebars.compile("*{{data.foo}}*");
|
||||
make('<script id="hb1" type="text/x-handlebars-template">*{{#.}}{{foo}}{{/.}}*</script>')
|
||||
btn.click();
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("*bar*");
|
||||
@ -56,4 +56,4 @@ describe("client-side-templates extension", function() {
|
||||
this.server.respond();
|
||||
btn.innerHTML.should.equal("*bar*");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,57 +0,0 @@
|
||||
# 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 endpoint 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."*
|
@ -1,22 +0,0 @@
|
||||
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.9.0
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/labstack/gommon v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
@ -1,52 +0,0 @@
|
||||
github.com/benpate/derp v0.20.0 h1:birPylsYOKkSMrcvNQEp+CmB7Dr1jnum8UXKv4oJvY8=
|
||||
github.com/benpate/derp v0.20.0/go.mod h1:ENjMpkMmxn9gAulMAggElVcD2kNrsbjFhoZK8fUlCxY=
|
||||
github.com/benpate/htmlconv v0.3.0 h1:UzIONnBJWtzBB3pYfKxlkq5JCsMq/+ImpAqNEmvFGcQ=
|
||||
github.com/benpate/htmlconv v0.3.0/go.mod h1:9P8tQ62E5L/KF2wTNI0szRTKs3b9WlOHj4Gxmp66rf4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/labstack/echo/v4 v4.9.0 h1:wPOF1CE6gvt/kmbMR4dGzWvHMPT+sAEUJOwOTtvITVY=
|
||||
github.com/labstack/echo/v4 v4.9.0/go.mod h1:xkCDAdFCIf8jsFQ5NnbK7oqaF/yU1A1X20Ltm0OvSks=
|
||||
github.com/labstack/gommon v0.3.1 h1:OomWaJXm7xR6L1HmEtGyQf26TEn7V6X88mktX9kee9o=
|
||||
github.com/labstack/gommon v0.3.1/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
|
||||
github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
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=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
|
||||
golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -1,350 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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
|
||||
|
||||
//go:embed static/data.json
|
||||
var dataBytes []byte
|
||||
|
||||
func main() {
|
||||
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
/// Load configuration file
|
||||
var data map[string][]interface{}
|
||||
|
||||
if err := json.Unmarshal(dataBytes, &data); err != nil {
|
||||
panic("Could not unmarshal data: " + err.Error())
|
||||
}
|
||||
|
||||
/// Configure Web Server
|
||||
|
||||
e := echo.New()
|
||||
|
||||
e.Static("/", "static")
|
||||
e.Static("/htmx", "../../src")
|
||||
|
||||
// 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)))
|
||||
e.GET("/albums.json", handleStream(makeStream(data["albums"], jsonFormatFunc)))
|
||||
e.GET("/todos.json", handleStream(makeStream(data["todos"], jsonFormatFunc)))
|
||||
e.GET("/users.json", handleStream(makeStream(data["users"], jsonFormatFunc)))
|
||||
|
||||
// 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)))
|
||||
e.GET("/albums.html", handleStream(makeStream(data["albums"], albumTemplate())))
|
||||
e.GET("/todos.html", handleStream(makeStream(data["todos"], todoTemplate())))
|
||||
e.GET("/users.html", handleStream(makeStream(data["users"], userTemplate())))
|
||||
|
||||
e.OPTIONS("/page/random", func(ctx echo.Context) error {
|
||||
ctx.Response().Header().Add("Connection", "keep-alive") // CORS headers
|
||||
ctx.Response().Header().Add("Access-Control-Allow-Origin", "*") // CORS headers
|
||||
ctx.Response().Header().Add("Access-Control-Allow-Methods", "GET") // CORS headers
|
||||
ctx.Response().Header().Add("Access-Control-Allow-Credentials", "true") // CORS headers
|
||||
ctx.Response().Header().Add("Access-Control-Allow-Headers", "*") // CORS headers
|
||||
ctx.NoContent(200)
|
||||
return nil
|
||||
})
|
||||
|
||||
e.GET("/page/random", func(ctx echo.Context) error {
|
||||
return pageHandler(ctx, rand.Int())
|
||||
})
|
||||
|
||||
e.GET("/page/:number", func(ctx echo.Context) error {
|
||||
|
||||
pageNumber, err := strconv.Atoi(ctx.Param("number"))
|
||||
|
||||
if err != nil {
|
||||
pageNumber = 1
|
||||
}
|
||||
|
||||
return pageHandler(ctx, pageNumber)
|
||||
})
|
||||
|
||||
e.GET("/revealed/:number", func(ctx echo.Context) error {
|
||||
|
||||
pageNumber, err := strconv.Atoi(ctx.Param("number"))
|
||||
|
||||
if err != nil {
|
||||
pageNumber = 1
|
||||
}
|
||||
|
||||
thisPage := strconv.Itoa(pageNumber)
|
||||
nextPage := strconv.Itoa(pageNumber + 1)
|
||||
random := strconv.Itoa(rand.Int())
|
||||
|
||||
template := htmlconv.CollapseWhitespace(`
|
||||
<div class="container" hx-get="/revealed/%s" hx-swap="afterend limit:10" hx-trigger="revealed">
|
||||
This is page %s<br><br>
|
||||
Randomly generated <b>HTML</b> %s<br><br>
|
||||
I wish I were a haiku.
|
||||
</div>`)
|
||||
|
||||
content := fmt.Sprintf(template, nextPage, thisPage, random)
|
||||
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)
|
||||
random := strconv.Itoa(rand.Int())
|
||||
|
||||
template := htmlconv.CollapseWhitespace(`
|
||||
<div>
|
||||
This is page %s<br><br>
|
||||
Randomly generated <b>HTML</b> %s<br><br>
|
||||
I wish I were a haiku.
|
||||
</div>`)
|
||||
|
||||
content := fmt.Sprintf(template, pageString, random)
|
||||
ctx.Response().Header().Add("Access-Control-Allow-Origin", "*") // CORS headers
|
||||
ctx.Response().Header().Add("Access-Control-Allow-Methods", "GET") // CORS headers
|
||||
ctx.Response().Header().Add("Access-Control-Allow-Headers", "*") // CORS headers
|
||||
ctx.Response().Header().Add("Access-Control-Allow-Credentials", "true") // CORS headers
|
||||
return ctx.HTML(200, content)
|
||||
}
|
||||
|
||||
func postTemplate() formatFunc {
|
||||
|
||||
// return templateFormatFunc("post.html", "DDD")
|
||||
return templateFormatFunc("post.html", `
|
||||
<div>
|
||||
<div class="bold">Post: {{.title}}</div>
|
||||
<div>{{.body}}</div>
|
||||
<div>id: {{.id}}</div>
|
||||
<div>user: {{.userId}}</div>
|
||||
<div>event: [[eventType]]</div>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
func commentTemplate() formatFunc {
|
||||
|
||||
// return templateFormatFunc("comment.html", "CCC")
|
||||
return templateFormatFunc("comment.html", `
|
||||
<div>
|
||||
<div class="bold">Comment: {{.name}}</div>
|
||||
<div>{{.email}}</div>
|
||||
<div>{{.body}}</div>
|
||||
<div>event: [[eventType]]</div>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
func albumTemplate() formatFunc {
|
||||
|
||||
return templateFormatFunc("album.html", `
|
||||
<div>
|
||||
<div class="bold">Album: {{.title}}</div>
|
||||
<div>id: {{.id}}</div>
|
||||
<div>event: [[eventType]]</div>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
func todoTemplate() formatFunc {
|
||||
|
||||
return templateFormatFunc("todo.html", `
|
||||
<div>
|
||||
<div class="bold">ToDo:{{.id}}: {{.title}}</div>
|
||||
<div>complete? {{.completed}}</div>
|
||||
<div>event: [[eventType]]</div>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
func userTemplate() formatFunc {
|
||||
|
||||
return templateFormatFunc("user.html", `
|
||||
<div>
|
||||
<div class="bold">User: {{.name}} / {{.username}}</div>
|
||||
<div>{{.email}}</div>
|
||||
<div>{{.address.street}} {{.address.suite}}<br>{{.address.city}}, {{.address.zipcode}}</div>
|
||||
<div>event: [[eventType]]</div>
|
||||
</div>`)
|
||||
}
|
||||
|
||||
// handleStream creates an echo.HandlerFunc that streams events from an eventSource to client browsers
|
||||
func handleStream(eventSource chan string) echo.HandlerFunc {
|
||||
|
||||
return func(ctx echo.Context) error {
|
||||
|
||||
w := ctx.Response().Writer
|
||||
|
||||
// Make sure that the writer supports flushing.
|
||||
f, ok := w.(http.Flusher)
|
||||
|
||||
if !ok {
|
||||
return derp.New(500, "handler.ServerSentEvent", "Streaming Not Supported")
|
||||
}
|
||||
|
||||
c := ctx.Request().Context()
|
||||
|
||||
// Set the headers related to event streaming.
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("Transfer-Encoding", "chunked")
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
|
||||
types := []string{}
|
||||
|
||||
if param := ctx.QueryParam("types"); param != "" {
|
||||
types = strings.Split(param, ",")
|
||||
}
|
||||
|
||||
// Don't close the connection, instead loop endlessly.
|
||||
for {
|
||||
|
||||
select {
|
||||
case <-c.Done():
|
||||
log.Println("HTTP connection just closed.")
|
||||
return nil
|
||||
|
||||
case message := <-eventSource:
|
||||
|
||||
var eventType string
|
||||
|
||||
if len(types) > 0 {
|
||||
eventType = types[rand.Int()%len(types)]
|
||||
fmt.Fprintf(w, "event: %s\n", eventType)
|
||||
}
|
||||
|
||||
message = strings.Replace(message, "\n", " ", -1)
|
||||
message = strings.Replace(message, "[[eventType]]", eventType, 1)
|
||||
|
||||
fmt.Fprintf(w, "data: %s\n\n", message)
|
||||
|
||||
// Flush the response. This is only possible if the response supports streaming.
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// makeStream loops through an array of interfaces
|
||||
func makeStream(data []interface{}, format formatFunc) chan string {
|
||||
|
||||
result := make(chan string)
|
||||
|
||||
go func() {
|
||||
|
||||
for {
|
||||
for _, record := range data {
|
||||
result <- format(record)
|
||||
time.Sleep((time.Duration(rand.Int() % 500)) * time.Millisecond)
|
||||
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func jsonFormatFunc(data interface{}) string {
|
||||
result, _ := json.Marshal(data)
|
||||
return string(result)
|
||||
}
|
||||
|
||||
func templateFormatFunc(name string, text string) formatFunc {
|
||||
|
||||
f, _ := template.New(name).Parse(htmlconv.CollapseWhitespace(text))
|
||||
|
||||
return func(data interface{}) string {
|
||||
|
||||
var buffer bytes.Buffer
|
||||
|
||||
f.Execute(&buffer, data)
|
||||
|
||||
return buffer.String()
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/stylesheet.css">
|
||||
<title></> htmx Realtime Test Server</title>
|
||||
<script src="/htmx/htmx.js"></script>
|
||||
<script src="/htmx/ext/sse.js"></script>
|
||||
<script src="/htmx/ext/ws.js"></script>
|
||||
|
||||
<script src="https://unpkg.com/hyperscript.org"></script>
|
||||
<script type="text/hyperscript">
|
||||
on click(target) from <#navigation a/>
|
||||
take .selected for target
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="header"></div>
|
||||
<div id="navigation" hx-target="#page" hx-push-url="false">
|
||||
<a href="index.html" class="selected" hx-boost="false">Introduction</a>
|
||||
|
||||
<div class="group">
|
||||
<a href="" hx-get="/ws-about.html">WebSockets</a>
|
||||
<a href="" hx-get="/ws-echo.html">Echo</a>
|
||||
<a href="" hx-get="/ws-heartbeat.html">Heartbeat</a>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<a href="" hx-get="/sse-about.html">Server Sent Events</a>
|
||||
<a href="" hx-get="/sse-simple.html">Simple</a>
|
||||
<a href="" hx-get="/sse-multiple.html">Multiple</a>
|
||||
<a href="" hx-get="/sse-multichannel.html">Multi-Channel</a>
|
||||
<a href="" hx-get="/sse-triggers.html">Event Trigger</a>
|
||||
<a href="" hx-get="/sse-target.html">Event Target</a>
|
||||
<a href="" hx-get="/sse-settle.html">Settling</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="page">
|
||||
<h1>Realtime Test Server</h1>
|
||||
|
||||
<h2>New Extensions</h2>
|
||||
<p>As of version 1.7, we have created two new extensions <b>ws.js</b> and <b>sse.js</b> to support realtime development in htmx. All new effort on WebSockets and Server Sent Events will occur in these extensions.</p>
|
||||
|
||||
<h2>Old Tags Deprecated</h2>
|
||||
<p>The existing <b>hx-ws</b> and <b>hx-sse</b> tags have been deprecated and will not receive any more updates. We plan to remove these two tags from the core library in htmx version 2.0.</p>
|
||||
|
||||
<h2>Try It For Yourself</h2>
|
||||
<p>Because extensions use a different calling syntax, there are minor differences in the way that this new code is invoked. This test server includes several demos / manual tests for each extension that you can try out for yourself. Each is presented side-by-side with test cases for the original code so that you can see the difference.</p>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,26 +0,0 @@
|
||||
<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".
|
||||
Each stream should populate its own container.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
<div hx-ext="sse" sse-connect="http://localhost/posts.html" sse-swap="message">Waiting for Posts...</div>
|
||||
</pre>
|
||||
|
||||
<h3>Test Cases</h3>
|
||||
|
||||
<div>
|
||||
<div class="container" hx-ext="sse" sse-connect="http://localhost/posts.html" sse-swap="message">Waiting for Posts...</div>
|
||||
<div class="container" hx-ext="sse" sse-connect="http://localhost/comments.html" sse-swap="message">Waiting for Comments...</div>
|
||||
<div class="container" hx-ext="sse" sse-connect="http://localhost/albums.html" sse-swap="message">Waiting for Albums...</div>
|
||||
<div class="container" hx-ext="sse" sse-connect="http://localhost/todos.html" sse-swap="message">Waiting for ToDos...</div>
|
||||
<div class="container" hx-ext="sse" sse-connect="http://localhost/users.html" sse-swap="message">Waiting for Users...</div>
|
||||
</div>
|
@ -1,26 +0,0 @@
|
||||
<h1>Simple Test</h1>
|
||||
|
||||
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||
<a role="tab" hx-get="/sse-simple.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" hx-get="/sse-simple-ext.html">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
<p>
|
||||
This page connects to several different Server Sent Event (SSE) streams, listening on the default event name "message".
|
||||
Each stream should populate its own container.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
<div hx-sse="connect:http://localhost/posts.html swap:message">Waiting for Posts...</div>
|
||||
</pre>
|
||||
|
||||
<h3>Test Cases</h3>
|
||||
|
||||
<div>
|
||||
<div class="container" hx-sse="connect:http://localhost/posts.html swap:message">Waiting for Posts...</div>
|
||||
<div class="container" hx-sse="connect:http://localhost/comments.html swap:message">Waiting for Comments...</div>
|
||||
<div class="container" hx-sse="connect:http://localhost/albums.html swap:message">Waiting for Albums...</div>
|
||||
<div class="container" hx-sse="connect:http://localhost/todos.html swap:message">Waiting for ToDos...</div>
|
||||
<div class="container" hx-sse="connect:http://localhost/users.html swap:message">Waiting for Users...</div>
|
||||
</div>
|
@ -1,26 +0,0 @@
|
||||
<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".
|
||||
Each event is used as a trigger for hx-get to load a random page from the server.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
<div hx-ext="sse" sse-connect="http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<div hx-get="http://localhost/page/random" hx-trigger="sse:Event1">Waiting for Posts...</div>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
<div hx-ext="sse" 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>
|
@ -1,26 +0,0 @@
|
||||
<h1>Event Trigger Test</h1>
|
||||
|
||||
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||
<a role="tab" hx-get="/sse-triggers.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" hx-get="/sse-triggers-ext.html">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
<p>
|
||||
This page connects to a single different Server Sent Event (SSE) streams, listening on events named "Event1", "Event2", "Event3", and "Event4".
|
||||
Each event is used as a trigger for hx-get to load a random page from the server.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
<div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<div hx-get="http://localhost/page/random" hx-trigger="sse:Event1">Waiting for Posts...</div>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
<div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<h3>Test Cases</h3>
|
||||
<div class="container" hx-get="http://localhost/page/random" hx-trigger="sse:Event1">Waiting for Posts in Event1 channel...</div>
|
||||
<div class="container" hx-get="http://localhost/page/random" hx-trigger="sse:Event2">Waiting for Posts in Event2 channel...</div>
|
||||
<div class="container" hx-get="http://localhost/page/random" hx-trigger="sse:Event3">Waiting for Posts in Event3 channel...</div>
|
||||
<div class="container" hx-get="http://localhost/page/random" hx-trigger="sse:Event4">Waiting for Posts in Event4 channel...</div>
|
||||
</div>
|
@ -1,15 +0,0 @@
|
||||
# Htmx Realtime Test Server
|
||||
|
||||
This package implements a realtime server for testing WebSockets and Server Sent Events (SSE) in htmx.
|
||||
|
||||
## What It Does
|
||||
|
||||
This server 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 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>`
|
||||
|
||||
## 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!
|
@ -1,23 +0,0 @@
|
||||
module github.com/benpate/ghost
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/labstack/echo/v4 v4.3.0
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/labstack/gommon v0.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
|
||||
)
|
@ -1,52 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/labstack/echo/v4 v4.3.0 h1:DCP6cbtT+Zu++K6evHOJzSgA2115cPMuCx0xg55q1EQ=
|
||||
github.com/labstack/echo/v4 v4.3.0/go.mod h1:PvmtTvhVqKDzDQy4d3bWzPjZLzom4iQbAZy2sgZ/qI8=
|
||||
github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0=
|
||||
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8=
|
||||
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
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/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=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
|
||||
github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
|
||||
github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
|
||||
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
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=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
@ -1,76 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func main() {
|
||||
e := echo.New()
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.Recover())
|
||||
|
||||
e.Static("/", "./static")
|
||||
e.Static("/htmx", "../../../src")
|
||||
|
||||
e.GET("/echo", wsEcho)
|
||||
e.GET("/heartbeat", wsHeartbeat)
|
||||
e.Logger.Fatal(e.Start(":80"))
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<svg width="389" height="82" viewBox="0 0 389 82" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M182.001 28.178V14.089V0H175.001H168.001L167.964 6.75C167.944 10.462 167.667 27.563 167.349 44.75L166.77 76H174.386H182.001V57.073V38.145L184.636 36.073C188.115 33.336 194.3 33.299 197.001 36C198.861 37.86 199.001 39.333 199.001 57V76H206.587H214.173L213.837 54.75C213.535 35.659 213.298 33.155 211.501 30.105C208.475 24.967 204.818 22.732 198.589 22.216C191.69 21.644 188.955 22.326 185.078 25.589L182.001 28.178Z" fill="#333333"/>
|
||||
<path d="M241.001 23V16.5V10H234.001H227.001V16.5V23H223.001H219.001V28.5V34H223.001H227.001L227.016 50.75C227.032 69.257 227.474 71.055 232.913 74.72C235.464 76.439 237.716 77 242.067 77C250.558 77 251.001 76.673 251.001 70.393V65L247.751 64.985C241.716 64.956 241.532 64.497 241.18 48.537L240.859 34H245.43H250.001V28.5V23H245.501H241.001Z" fill="#333333"/>
|
||||
<path d="M299.429 27.201C298.577 25.984 296.579 24.316 294.99 23.494C290.431 21.137 282.086 21.581 276.65 24.472C272.192 26.842 272.001 26.863 272.001 24.972C272.001 23.197 271.301 23 265.001 23H258.001V49.5V76H265.001H272.001V57.455C272.001 39.098 272.026 38.884 274.456 36.455C276.22 34.69 278.003 34 280.801 34C287.696 34 288.001 34.966 288.001 56.829V76H295.501H303.001V57.455C303.001 39.098 303.026 38.884 305.456 36.455C308.565 33.345 314.127 33.126 317.001 36C318.861 37.86 319.001 39.333 319.001 57V76H326.501H334.001L333.995 56.75C333.992 45.986 333.509 35.737 332.9 33.5C330.814 25.849 325.253 21.998 316.301 22.004C310.396 22.008 305.023 24.22 302.24 27.794C301.098 29.259 300.832 29.203 299.429 27.201Z" fill="#333333"/>
|
||||
<path d="M361.928 33.395C360.349 30.703 358.54 27.263 357.908 25.75C356.791 23.078 356.535 23 348.821 23C344.455 23 341.077 23.338 341.315 23.75C341.552 24.162 344.932 29.997 348.825 36.715L355.904 48.93L348.021 62.465L340.139 76H348.002H355.865L360.146 68.073C362.501 63.713 364.742 60.34 365.127 60.578C365.511 60.815 367.677 64.382 369.939 68.505L374.053 76H381.527C385.638 76 389.001 75.768 389.001 75.483C389.001 75.199 385.598 69.113 381.439 61.958L373.877 48.949L381.403 35.974L388.929 23L381.215 23.011L373.501 23.021L369.15 30.655L364.798 38.289L361.928 33.395Z" fill="#3D72D7"/>
|
||||
<path d="M74.3377 44.7953C81.2334 24.3322 87.0228 6.91645 87.2037 6.09245C87.4555 4.94086 83.3472 4.52536 79.4177 4.29664L74.3028 4L61.7576 41.2516C54.8569 61.7407 48.9026 79.2903 48.5259 80.2521C47.9233 81.7873 48.5059 82 53.3199 82H61.7995L74.3377 44.7953Z" fill="#3D72D7"/>
|
||||
<path d="M43.0572 35.8912V28.9816C43.0572 24.0716 42.6954 22.1 41.808 22.1649C41.1214 22.2158 31.5644 26.168 20.5712 30.9492L0.583636 39.6406L0.291818 45.6143L0 51.588L20.7791 60.3164C32.208 65.1166 41.8959 69.0378 42.3077 69.0298C42.7194 69.0218 43.0572 65.9206 43.0572 62.1372V55.2576L29.4747 50.4884L15.8921 45.7182L25.2273 42.4062C30.3611 40.5844 36.4733 38.3731 38.8098 37.4922L43.0572 35.8912Z" fill="#333333"/>
|
||||
<path d="M134.986 39.5517L115.255 31.3467C104.403 26.8342 94.7381 22.854 93.7757 22.5005C92.2156 21.9282 92.0267 22.6143 92.0267 28.8627V35.8672L100.272 38.8216C104.806 40.4466 110.878 42.5471 113.763 43.4909C116.648 44.4338 119.01 45.3357 119.01 45.4935C119.01 45.6523 112.939 47.9145 105.518 50.5203L92.0267 55.2576V62.1372C92.0267 65.9206 92.3645 69.0218 92.7763 69.0298C93.188 69.0378 102.858 65.1455 114.263 60.3813L135 51.7179L134.993 45.6343L134.986 39.5517Z" fill="#333333"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.3 KiB |
@ -1,63 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="/stylesheet.css">
|
||||
<title></> htmx WebSocket Server</title>
|
||||
<script src="/htmx/htmx.js"></script>
|
||||
<script src="/htmx/ext/ws.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="ws-heartbeat.html">Heartbeat Test</a>
|
||||
<a href="" hx-get="ws-echo.html">Echo Test</a>
|
||||
</div>
|
||||
<div id="page">
|
||||
<h1>WebSockets Extension Tests</h1>
|
||||
|
||||
<p>As of version 1.7, WebSocket support has been moved out of the core htmx library and into an extension. This server runs a test suite for the htmx WebSocket extension.</p>
|
||||
<p>This extension connects to a WebSocket echo server and can send and receive messages to and from the server.</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">ws-connect</td>
|
||||
<td>Connects to a WebSocket server. Attribute value must begin with ws:// wss://</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="bold nowrap">ws-send</td>
|
||||
<td>Add to a form to submit form data to the websocket server instead of to an HTTP server.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h3>Example Code</h3>
|
||||
|
||||
<pre class="code">
|
||||
<body hx-ext="ws">
|
||||
<div ws-connect="wss://my.websocket.server.com"></div>
|
||||
|
||||
<form ws-send>
|
||||
<input name="WebSocketMessage">
|
||||
<form/>
|
||||
</body>
|
||||
</pre>
|
||||
<h3>WebSocket Resources</h3>
|
||||
<ul>
|
||||
<li><a href="https://en.wikipedia.org/wiki/WebSocket">Wikipedia</a></li>
|
||||
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API">MDN Web Docs</a></li>
|
||||
<li><a href="https://caniuse.com/eventsource">Can I Use?</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,112 +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;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#navigation > a {
|
||||
display:block;
|
||||
cursor: pointer;
|
||||
text-decoration:none;
|
||||
padding:10px 20px;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
padding:5px 10px;
|
||||
border:none;
|
||||
border-radius:5px;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background-color:#3465a4;
|
||||
color:white;
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<svg width="391" height="82" viewBox="0 0 391 82" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M184.001 28.178V14.089V0H177.001H170.001L169.964 6.75C169.944 10.462 169.667 27.563 169.349 44.75L168.77 76H176.386H184.001V57.073V38.145L186.636 36.073C190.115 33.336 196.3 33.299 199.001 36C200.861 37.86 201.001 39.333 201.001 57V76H208.587H216.173L215.837 54.75C215.535 35.659 215.298 33.155 213.501 30.105C210.475 24.967 206.818 22.732 200.589 22.216C193.69 21.644 190.955 22.326 187.078 25.589L184.001 28.178Z" fill="#F5F5F5"/>
|
||||
<path d="M243.001 23V16.5V10H236.001H229.001V16.5V23H225.001H221.001V28.5V34H225.001H229.001L229.016 50.75C229.032 69.257 229.474 71.055 234.913 74.72C237.464 76.439 239.716 77 244.067 77C252.558 77 253.001 76.673 253.001 70.393V65L249.751 64.985C243.716 64.956 243.532 64.497 243.18 48.537L242.859 34H247.43H252.001V28.5V23H247.501H243.001Z" fill="#F5F5F5"/>
|
||||
<path d="M301.429 27.201C300.577 25.984 298.579 24.316 296.99 23.494C292.431 21.137 284.086 21.581 278.65 24.472C274.192 26.842 274.001 26.863 274.001 24.972C274.001 23.197 273.301 23 267.001 23H260.001V49.5V76H267.001H274.001V57.455C274.001 39.098 274.026 38.884 276.456 36.455C278.22 34.69 280.003 34 282.801 34C289.696 34 290.001 34.966 290.001 56.829V76H297.501H305.001V57.455C305.001 39.098 305.026 38.884 307.456 36.455C310.565 33.345 316.127 33.126 319.001 36C320.861 37.86 321.001 39.333 321.001 57V76H328.501H336.001L335.995 56.75C335.992 45.986 335.509 35.737 334.9 33.5C332.814 25.849 327.253 21.998 318.301 22.004C312.396 22.008 307.023 24.22 304.24 27.794C303.098 29.259 302.832 29.203 301.429 27.201Z" fill="#F5F5F5"/>
|
||||
<path d="M363.928 33.395C362.349 30.703 360.54 27.263 359.908 25.75C358.791 23.078 358.535 23 350.821 23C346.455 23 343.077 23.338 343.315 23.75C343.552 24.162 346.932 29.997 350.825 36.715L357.904 48.93L350.021 62.465L342.139 76H350.002H357.865L362.146 68.073C364.501 63.713 366.742 60.34 367.127 60.578C367.511 60.815 369.677 64.382 371.939 68.505L376.053 76H383.527C387.638 76 391.001 75.768 391.001 75.483C391.001 75.199 387.598 69.113 383.439 61.958L375.877 48.949L383.403 35.974L390.929 23L383.215 23.011L375.501 23.021L371.15 30.655L366.798 38.289L363.928 33.395Z" fill="#3D72D7"/>
|
||||
<path d="M75.1218 44.7953C82.0175 24.3322 87.8069 6.91645 87.9877 6.09245C88.2396 4.94086 84.1313 4.52536 80.2017 4.29664L75.0869 4L62.5417 41.2516C55.641 61.7407 49.6867 79.2903 49.31 80.2521C48.7073 81.7873 49.29 82 54.104 82H62.5836L75.1218 44.7953Z" fill="#3D72D7"/>
|
||||
<path d="M43.8414 35.8912V28.9816C43.8414 24.0716 43.4796 22.1 42.5921 22.1649C41.9056 22.2158 32.3485 26.168 21.3553 30.9492L1.36779 39.6406L1.07597 45.6143L0.784149 51.588L21.5632 60.3164C32.9921 65.1166 42.6801 69.0378 43.0918 69.0298C43.5036 69.0218 43.8414 65.9206 43.8414 62.1372V55.2576L30.2588 50.4884L16.6763 45.7182L26.0114 42.4062C31.1453 40.5844 37.2575 38.3731 39.594 37.4922L43.8414 35.8912Z" fill="#F5F5F5"/>
|
||||
<path d="M135.77 39.5517L116.039 31.3467C105.187 26.8342 95.5222 22.854 94.5598 22.5005C92.9998 21.9282 92.8109 22.6143 92.8109 28.8627V35.8672L101.056 38.8216C105.59 40.4466 111.662 42.5471 114.547 43.4909C117.433 44.4338 119.794 45.3357 119.794 45.4935C119.794 45.6523 113.723 47.9145 106.302 50.5203L92.8109 55.2576V62.1372C92.8109 65.9206 93.1487 69.0218 93.5604 69.0298C93.9722 69.0378 103.642 65.1455 115.047 60.3813L135.784 51.7179L135.777 45.6343L135.77 39.5517Z" fill="#F5F5F5"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.3 KiB |
41
www/static/test/ws-sse/README.md
Normal file
41
www/static/test/ws-sse/README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# WebSocket and Server-Side Events live test suite
|
||||
This package implements a realtime server for testing WebSockets and Server Sent Events (SSE) in htmx.
|
||||
|
||||
## How to Use This Server
|
||||
From this directory, run `node server.js`.
|
||||
|
||||
If you want to hotreload, I recommend using `nodemon`, which you can install with `npm install -g
|
||||
nodemon`, and then run `nodemon -e js,html server.js`.
|
||||
|
||||
## Web Sockets
|
||||
When htmx 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
|
||||
When the echo endpoint 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 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.
|
||||
|
||||
### 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
|
||||
This test suite was originally written by Ben Pate, and updated to run in nodeJS by Alex Petros.
|
||||
|
||||
It is inspired by [jsonplaceholder](https://jsonplaceholder.typicode.com) -- *"a free online REST API that you can use whenever you need some fake data."*
|
244
www/static/test/ws-sse/server.js
Normal file
244
www/static/test/ws-sse/server.js
Normal file
@ -0,0 +1,244 @@
|
||||
import * as http from 'node:http'
|
||||
import * as path from 'node:path'
|
||||
import * as fs from 'node:fs/promises'
|
||||
|
||||
import { WebSocketServer } from 'ws'
|
||||
|
||||
// Define some string and number constants
|
||||
const HOSTNAME = '127.0.0.1';
|
||||
const PORT = 8080;
|
||||
const DATA = JSON.parse(await fs.readFile('./static/data.json'))
|
||||
const SITE_BASE = (await fs.readFile('./static/site-base.html')).toString()
|
||||
|
||||
// Define the websockets
|
||||
const ECHO_WS = createWebSocket((ws) => {
|
||||
ws.on('message', (message) => {
|
||||
const data = JSON.parse(message.toString())
|
||||
ws.send(`<div id=idMessage>${data.message}</div>`)
|
||||
})
|
||||
})
|
||||
const HEARTBEAT_WS = createWebSocket((ws) => {
|
||||
ws.interval = setInterval(() => {
|
||||
const num = Math.trunc(Math.random() * 10**10)
|
||||
ws.send(`<div id=idMessage>${num}</div>`)
|
||||
}, 1000)
|
||||
}, (ws) => clearInterval(ws.interval))
|
||||
|
||||
|
||||
// Define the server
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
await handleRequest(req, res)
|
||||
} catch (error) {
|
||||
console.error(`Error serving ${req.url}`)
|
||||
console.error(req.body)
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
// This handles all the non-websocket requests
|
||||
async function handleRequest (req, res) {
|
||||
// If the URL starts with htmx, serve the src/ root version of htmx
|
||||
if (req.url.startsWith('/htmx')) {
|
||||
const resource = req.url.substring(6)
|
||||
res.setHeader('Content-Type', 'text/javascript')
|
||||
const fp = path.join('../../src', resource)
|
||||
return serveFile(res, fp)
|
||||
}
|
||||
|
||||
// If the URL matches one of these, it's an event stream
|
||||
if (req.url.startsWith("/posts.html")) return servePosts(req, res)
|
||||
if (req.url === "/comments.html") return makeStream(req, res, DATA.comments, formatComment)
|
||||
if (req.url === "/albums.html") return makeStream(req, res, DATA.albums, formatAlbum)
|
||||
if (req.url === "/todos.html") return makeStream(req, res, DATA.todos, formatTodo)
|
||||
if (req.url === "/users.html") return makeStream(req, res, DATA.users, formatUser)
|
||||
|
||||
// Randomly-generated HTML
|
||||
if (req.url === "/page/random") return serveRandomHtml(req, res)
|
||||
|
||||
// Otherwise, attempt to serve the file from ./static and return a 404 on failure
|
||||
try {
|
||||
await serveFileFromStatic(req, res)
|
||||
} catch (error) {
|
||||
sendNotFound(res)
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the websockets
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
if (request.url === '/echo') ECHO_WS.handle(request, socket, head)
|
||||
if (request.url === '/heartbeat') HEARTBEAT_WS.handle(request, socket, head)
|
||||
})
|
||||
|
||||
// Start listening
|
||||
server.listen(PORT, HOSTNAME, () => {
|
||||
console.log('Loading the WebSocket / Server-Side Event Tests...');
|
||||
console.log(`You can run them at http://${HOSTNAME}:${PORT}/`);
|
||||
})
|
||||
|
||||
function createWebSocket (connectionFunc, closeFunc) {
|
||||
const server = new WebSocketServer({ noServer: true })
|
||||
server.on('connection', connectionFunc)
|
||||
if (closeFunc) server.on('close', closeFunc)
|
||||
|
||||
const handle = (request, socket, head) => {
|
||||
server.handleUpgrade(request, socket, head, (ws) => {
|
||||
server.emit('connection', ws, request)
|
||||
})
|
||||
}
|
||||
return { handle }
|
||||
}
|
||||
|
||||
async function serveFileFromStatic (req, res) {
|
||||
// For the root, serve the static index.html file
|
||||
const resource = req.url === '/' ? '/index.html' : req.url
|
||||
|
||||
let fp = path.join('./static/', resource)
|
||||
let lstat = await fs.lstat(fp)
|
||||
|
||||
// If it's a directory, re-set the fp to be the index.html of that directory
|
||||
if (lstat.isDirectory()) {
|
||||
fp = path.join(fp, 'index.html')
|
||||
lstat = await fs.lstat(fp)
|
||||
}
|
||||
|
||||
if (!lstat.isFile) return sendNotFound(res)
|
||||
|
||||
const withBase = fp.endsWith('.html')
|
||||
return serveFile(res, fp, withBase)
|
||||
}
|
||||
|
||||
async function serveFile (res, fp, withBase) {
|
||||
try {
|
||||
const file = await fs.readFile(fp)
|
||||
let text = file.toString()
|
||||
if (withBase) text = SITE_BASE + text
|
||||
res.end(text)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
sendNotFound(res)
|
||||
}
|
||||
}
|
||||
|
||||
function servePosts (req, res) {
|
||||
// Why do we have to specify a fake protocol here? Because WHATWG doesn't support relative URLs
|
||||
// Maddening discussion here: https://github.com/whatwg/url/issues/531
|
||||
const url = new URL(req.url, "thismessage:/")
|
||||
const types = url.searchParams?.get('types')
|
||||
|
||||
const numEvents = types ? types.split(',').length : 0
|
||||
makeStream(req, res, DATA.posts, formatPost, numEvents)
|
||||
}
|
||||
|
||||
function sendNotFound(res) {
|
||||
res.statusCode = 404
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
res.end('404 NOT FOUND')
|
||||
}
|
||||
|
||||
function makeStream(req, res, arr, formatFunc, numEvents = 0) {
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
Connection: 'keep-alive',
|
||||
'Cache-Control': 'no-cache'
|
||||
})
|
||||
|
||||
// Make the intervals somewhat random, between 200 and 400ms
|
||||
// We have some tests that create multiple streams at once, so this ensures they're all visibile
|
||||
const intervalLength = Math.floor(Math.random() * 200) + 200
|
||||
|
||||
let i = 0
|
||||
const interval = setInterval(() => {
|
||||
if (i == arr.length) i = 0
|
||||
|
||||
const item = arr[i]
|
||||
try {
|
||||
const evenNum = Math.floor(Math.random() * numEvents) + 1
|
||||
const eventName = numEvents > 0 ? `Event${evenNum}` : '(none)'
|
||||
item.event = eventName
|
||||
|
||||
const formattedData = formatFunc(item).replace(/\n/g, ' ')
|
||||
const event = `${numEvents > 0 ? `event: ${eventName}\n` : ''}data: ${formattedData}\n\n`
|
||||
res.write(event)
|
||||
i++
|
||||
} catch (error) {
|
||||
// Stop the interval if it errors for any reason
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, intervalLength)
|
||||
|
||||
req.on('close', () => {
|
||||
res.end('OK')
|
||||
clearInterval(interval)
|
||||
})
|
||||
}
|
||||
|
||||
function serveRandomHtml(_req, res) {
|
||||
const page_num = Math.trunc(Math.random() * 10**10)
|
||||
const html_num = Math.trunc(Math.random() * 10**10)
|
||||
const html = `
|
||||
<div>
|
||||
This is page ${page_num}
|
||||
<br><br>
|
||||
Randomly generated <b>HTML</b> ${html_num}
|
||||
<br><br>
|
||||
I wish I were a haiku.
|
||||
</div>
|
||||
`
|
||||
res.end(html)
|
||||
}
|
||||
|
||||
function formatPost (post) {
|
||||
return `
|
||||
<div>
|
||||
<div class="bold">Post: ${post.title}</div>
|
||||
<div>${post.body}</div>
|
||||
<div>id: ${post.id}</div>
|
||||
<div>user: ${post.userId}</div>
|
||||
<div>event: ${post.event}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function formatComment (comment) {
|
||||
return `
|
||||
<div>
|
||||
<div class="bold">Comment: ${comment.name}</div>
|
||||
<div>${comment.email}</div>
|
||||
<div>id: ${comment.body}</div>
|
||||
<div>event: ${comment.event}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function formatAlbum (album) {
|
||||
return `
|
||||
<div>
|
||||
<div class="bold">Album: ${album.title}</div>
|
||||
<div>id: ${album.id}</div>
|
||||
<div>event: ${album.event}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function formatTodo (todo) {
|
||||
return `
|
||||
<div>
|
||||
<div class="bold">To-Do: ${todo.title}</div>
|
||||
<div>complete? ${todo.completed}</div>
|
||||
<div>event: ${todo.event}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
function formatUser (user) {
|
||||
return `
|
||||
<div>
|
||||
<div class="bold">User: ${user.name}</div>
|
||||
<div>${user.email}</div>
|
||||
<div>${user.address.street} ${user.address.suite}<br>${user.address.city}, ${user.address.zipcode}</div>
|
||||
<div>event: ${user.event}</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
12
www/static/test/ws-sse/static/index.html
Normal file
12
www/static/test/ws-sse/static/index.html
Normal file
@ -0,0 +1,12 @@
|
||||
<main>
|
||||
<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>
|
||||
</main>
|
37
www/static/test/ws-sse/static/site-base.html
Normal file
37
www/static/test/ws-sse/static/site-base.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<link rel="stylesheet" href="/stylesheet.css">
|
||||
<title></> htmx Realtime Test Server</title>
|
||||
<script src="/htmx/htmx.js"></script>
|
||||
<script src="/htmx/ext/sse.js"></script>
|
||||
<script src="/htmx/ext/ws.js"></script>
|
||||
|
||||
<div id="header"></div>
|
||||
<div id="navigation">
|
||||
<a href="/">Introduction</a>
|
||||
|
||||
<div class="group">
|
||||
<a href="/ws-about.html">WebSockets</a>
|
||||
<a href="/ws-echo.html">Echo</a>
|
||||
<a href="/ws-heartbeat.html">Heartbeat</a>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<a href="/sse-about.html">Server Sent Events</a>
|
||||
<a href="/sse-simple.html">Simple</a>
|
||||
<a href="/sse-multiple.html">Multiple</a>
|
||||
<a href="/sse-multichannel.html">Multi-Channel</a>
|
||||
<a href="/sse-triggers.html">Event Trigger</a>
|
||||
<a href="/sse-target.html">Event Target</a>
|
||||
<a href="/sse-settle.html">Settling</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// This is a tiny hack because I'm simply prepending this file to every page
|
||||
// Obviously you could do this with a template engine instead but I'm not using a template engine
|
||||
const navlinks = document.querySelectorAll('#navigation a')
|
||||
navlinks.forEach(link => {
|
||||
const locWithoutExt = window.location.toString().replace('-ext', '')
|
||||
if (link.href === locWithoutExt) link.classList.add('selected')
|
||||
})
|
||||
</script>
|
@ -1,3 +1,4 @@
|
||||
<main>
|
||||
<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>
|
||||
@ -32,3 +33,4 @@
|
||||
<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>
|
||||
</main>
|
@ -1,8 +1,9 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/sse-multichannel.html">Legacy Style</a>
|
||||
<a role="tab" href="/sse-multichannel-ext.html" aria-selected="true">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
@ -16,10 +17,11 @@
|
||||
<div sse-swap="Event1">Waiting for Posts in Event1 channel...</div>
|
||||
</div>
|
||||
</pre>
|
||||
<div hx-ext="sse" sse-connect="http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<div hx-ext="sse" sse-connect="/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<h3>Test Cases</h3>
|
||||
<div class="container" sse-swap="Event1">Waiting for Posts in Event1 channel...</div>
|
||||
<div class="container" sse-swap="Event2">Waiting for Posts in Event2 channel...</div>
|
||||
<div class="container" sse-swap="Event3">Waiting for Posts in Event3 channel...</div>
|
||||
<div class="container" sse-swap="Event4">Waiting for Posts in Event4 channel...</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@ -1,8 +1,9 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/sse-multichannel.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" href="/sse-multichannel-ext.html">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
@ -16,10 +17,11 @@
|
||||
<div hx-sse="swap:Event1">Waiting for Posts in Event1 channel...</div>
|
||||
</div>
|
||||
</pre>
|
||||
<div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<div hx-sse="connect:/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>
|
||||
</div>
|
||||
</main>
|
@ -1,13 +1,14 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/sse-multiple.html">Legacy Style</a>
|
||||
<a role="tab" href="/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.
|
||||
This page connects to a single Server Sent Event (SSE) streams, but listens to multiple events.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
@ -17,6 +18,7 @@
|
||||
<h3>Test Cases</h3>
|
||||
|
||||
<div>
|
||||
<div class="container" hx-ext="sse" sse-connect="http://localhost/posts.html?types=Event1,Event2" sse-swap="Event1,Event2">Waiting for Posts...</div>
|
||||
<div class="container" hx-ext="sse" sse-connect="/posts.html?types=Event1,Event2" sse-swap="Event1,Event2">Waiting for Posts...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@ -1,13 +1,14 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/sse-multiple.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" href="/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.
|
||||
This page connects to a single Server Sent Event (SSE) streams, but listens to multiple events.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
@ -17,6 +18,7 @@
|
||||
<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 class="container" hx-sse="connect:/posts.html?types=Event1,Event2 swap:Event1 swap:Event2">Waiting for Posts...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@ -1,13 +1,14 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/sse-settle.html">Legacy Style</a>
|
||||
<a role="tab" href="/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.
|
||||
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>
|
||||
@ -18,7 +19,7 @@
|
||||
</pre>
|
||||
|
||||
<h3>Test Cases</h3>
|
||||
<div hx-ext="sse" sse-connect="http://localhost/comments.html">
|
||||
<div hx-ext="sse" sse-connect="/comments.html">
|
||||
<div class="container" sse-swap="message" hx-swap="innerHTML settle:50ms">Waiting for Comments...</div>
|
||||
<div class="container" sse-swap="message" hx-swap="innerHTML settle:100ms">Waiting for Comments...</div>
|
||||
<div class="container" sse-swap="message" hx-swap="innerHTML settle:150ms">Waiting for Comments...</div>
|
||||
@ -26,3 +27,4 @@
|
||||
<div class="container" sse-swap="message" hx-swap="innerHTML settle:250ms">Waiting for Comments...</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@ -1,13 +1,14 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/sse-settle.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" href="/sse-settle-ext.html">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
<p>
|
||||
This page connects to a single different Server Sent Event (SSE) stream.
|
||||
This page connects to a single different Server Sent Event (SSE) stream.
|
||||
Multiple containers all listen for the same default "message" event name, but using different values for hx-swap.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
@ -18,7 +19,7 @@
|
||||
</pre>
|
||||
|
||||
<h3>Test Cases</h3>
|
||||
<div hx-sse="connect:http://localhost/comments.html">
|
||||
<div hx-sse="connect:/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>
|
||||
@ -26,3 +27,4 @@
|
||||
<div class="container" hx-sse="swap:message" hx-swap="innerHTML settle:250ms">Waiting for Comments...</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
28
www/static/test/ws-sse/static/sse-simple-ext.html
Normal file
28
www/static/test/ws-sse/static/sse-simple-ext.html
Normal file
@ -0,0 +1,28 @@
|
||||
<main>
|
||||
<h1>Simple Test</h1>
|
||||
|
||||
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||
<a role="tab" href="/sse-simple.html">Legacy Style</a>
|
||||
<a role="tab" href="/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".
|
||||
Each stream should populate its own container.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
<div hx-ext="sse" sse-connect="http://localhost/posts.html" sse-swap="message">Waiting for Posts...</div>
|
||||
</pre>
|
||||
|
||||
<h3>Test Cases</h3>
|
||||
|
||||
<div>
|
||||
<div class="container" hx-ext="sse" sse-connect="/posts.html" sse-swap="message">Waiting for Posts...</div>
|
||||
<div class="container" hx-ext="sse" sse-connect="/comments.html" sse-swap="message">Waiting for Comments...</div>
|
||||
<div class="container" hx-ext="sse" sse-connect="/albums.html" sse-swap="message">Waiting for Albums...</div>
|
||||
<div class="container" hx-ext="sse" sse-connect="/todos.html" sse-swap="message">Waiting for ToDos...</div>
|
||||
<div class="container" hx-ext="sse" sse-connect="/users.html" sse-swap="message">Waiting for Users...</div>
|
||||
</div>
|
||||
</main>
|
28
www/static/test/ws-sse/static/sse-simple.html
Normal file
28
www/static/test/ws-sse/static/sse-simple.html
Normal file
@ -0,0 +1,28 @@
|
||||
<main>
|
||||
<h1>Simple Test</h1>
|
||||
|
||||
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||
<a role="tab" href="/sse-simple.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" href="/sse-simple-ext.html">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
<p>
|
||||
This page connects to several different Server Sent Event (SSE) streams, listening on the default event name "message".
|
||||
Each stream should populate its own container.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
<div hx-sse="connect:http://localhost/posts.html swap:message">Waiting for Posts...</div>
|
||||
</pre>
|
||||
|
||||
<h3>Test Cases</h3>
|
||||
|
||||
<div>
|
||||
<div class="container" hx-sse="connect:/posts.html swap:message">Waiting for Posts...</div>
|
||||
<div class="container" hx-sse="connect:/comments.html swap:message">Waiting for Comments...</div>
|
||||
<div class="container" hx-sse="connect:/albums.html swap:message">Waiting for Albums...</div>
|
||||
<div class="container" hx-sse="connect:/todos.html swap:message">Waiting for ToDos...</div>
|
||||
<div class="container" hx-sse="connect:/users.html swap:message">Waiting for Users...</div>
|
||||
</div>
|
||||
</main>
|
@ -1,13 +1,14 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/sse-target.html">Legacy Style</a>
|
||||
<a role="tab" href="/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.
|
||||
This page connects to several different different Server Sent Event (SSE) stream.
|
||||
Contents from each stream are sent to a single DOM element.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
@ -27,3 +28,4 @@
|
||||
</div>
|
||||
|
||||
<div class="container" id="otherNode">Waiting for records from any of: Posts/Comments/Albums/ToDos/Users</div>
|
||||
</main>
|
@ -1,12 +1,14 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/sse-target.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" href="/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>
|
||||
</p>
|
||||
</main>
|
28
www/static/test/ws-sse/static/sse-triggers-ext.html
Normal file
28
www/static/test/ws-sse/static/sse-triggers-ext.html
Normal file
@ -0,0 +1,28 @@
|
||||
<main>
|
||||
<h1>Event Trigger Test</h1>
|
||||
|
||||
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||
<a role="tab" href="/sse-triggers.html">Legacy Style</a>
|
||||
<a role="tab" href="/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".
|
||||
Each event is used as a trigger for hx-get to load a random page from the server.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
<div hx-ext="sse" sse-connect="http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<div hx-get="http://localhost/page/random" hx-trigger="sse:Event1">Waiting for Posts...</div>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
<div hx-ext="sse" sse-connect="/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<h3>Test Cases</h3>
|
||||
<div class="container" hx-get="/page/random" hx-trigger="sse:Event1">Waiting for Posts in Event1 channel...</div>
|
||||
<div class="container" hx-get="/page/random" hx-trigger="sse:Event2">Waiting for Posts in Event2 channel...</div>
|
||||
<div class="container" hx-get="/page/random" hx-trigger="sse:Event3">Waiting for Posts in Event3 channel...</div>
|
||||
<div class="container" hx-get="/page/random" hx-trigger="sse:Event4">Waiting for Posts in Event4 channel...</div>
|
||||
</div>
|
||||
</main>
|
28
www/static/test/ws-sse/static/sse-triggers.html
Normal file
28
www/static/test/ws-sse/static/sse-triggers.html
Normal file
@ -0,0 +1,28 @@
|
||||
<main>
|
||||
<h1>Event Trigger Test</h1>
|
||||
|
||||
<div role="tablist" hx-target="#page" hx-push-url="false">
|
||||
<a role="tab" href="/sse-triggers.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" href="/sse-triggers-ext.html">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
<p>
|
||||
This page connects to a single different Server Sent Event (SSE) streams, listening on events named "Event1", "Event2", "Event3", and "Event4".
|
||||
Each event is used as a trigger for hx-get to load a random page from the server.
|
||||
</p>
|
||||
<h3>Example HTML</h3>
|
||||
<pre class="code">
|
||||
<div hx-sse="connect:http://localhost/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<div hx-get="http://localhost/page/random" hx-trigger="sse:Event1">Waiting for Posts...</div>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
<div hx-sse="connect:/posts.html?types=Event1%2cEvent2%2cEvent3%2cEvent4">
|
||||
<h3>Test Cases</h3>
|
||||
<div class="container" hx-get="/page/random" hx-trigger="sse:Event1">Waiting for Posts in Event1 channel...</div>
|
||||
<div class="container" hx-get="/page/random" hx-trigger="sse:Event2">Waiting for Posts in Event2 channel...</div>
|
||||
<div class="container" hx-get="/page/random" hx-trigger="sse:Event3">Waiting for Posts in Event3 channel...</div>
|
||||
<div class="container" hx-get="/page/random" hx-trigger="sse:Event4">Waiting for Posts in Event4 channel...</div>
|
||||
</div>
|
||||
</main>
|
@ -117,7 +117,7 @@ body {
|
||||
font-weight:bold;
|
||||
}
|
||||
|
||||
#page {
|
||||
main {
|
||||
margin: 50px;
|
||||
padding-left:200px;
|
||||
}
|
||||
@ -158,7 +158,7 @@ body {
|
||||
background-color:var(--gray05);
|
||||
border:solid 1px var(--gray30);
|
||||
border-bottom: solid 1px var(--gray40);
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
border-radius: 4px 4px 0px 0px;
|
||||
color: var(--gray50);
|
||||
font-family: inherit;
|
||||
font-size:1.1rem;
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
@ -1,3 +1,4 @@
|
||||
<main>
|
||||
<h1>WebSockets</h1>
|
||||
|
||||
<p>WebSockets create a fast, bi-directional connection between your server and a client's web browser.</p>
|
||||
@ -32,3 +33,4 @@
|
||||
<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>
|
||||
</main>
|
@ -1,8 +1,9 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/ws-echo.html">Legacy Style</a>
|
||||
<a role="tab" href="/ws-echo-ext.html" aria-selected="true">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
@ -23,7 +24,7 @@ back to you in a separate message</p>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
<div class="elastic container" hx-ext="ws" ws-connect="ws://localhost/echo">
|
||||
<div class="elastic container" hx-ext="ws" ws-connect="/echo">
|
||||
|
||||
<form ws-send="">
|
||||
<h3>Send a Message</h3>
|
||||
@ -36,4 +37,5 @@ back to you in a separate message</p>
|
||||
<br>
|
||||
<h3>Receive a Message</h3>
|
||||
<div id="idMessage"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
@ -1,8 +1,9 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/ws-echo.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" href="/ws-echo-ext.html">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
@ -23,7 +24,7 @@ back to you in a separate message</p>
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
<div class="elastic container" hx-ws="connect:ws://localhost/echo">
|
||||
<div class="elastic container" hx-ws="connect:/echo">
|
||||
|
||||
<form hx-ws="send">
|
||||
<h3>Send a Message</h3>
|
||||
@ -36,4 +37,5 @@ back to you in a separate message</p>
|
||||
<br>
|
||||
<h3>Receive a Message</h3>
|
||||
<div id="idMessage"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main/>
|
@ -1,8 +1,9 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/ws-heartbeat.html">Legacy Style</a>
|
||||
<a role="tab" href="/ws-heartbeat-ext.html" aria-selected="true">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
@ -16,8 +17,9 @@
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
<div class="container" hx-ext="ws" ws-connect="ws://localhost/heartbeat">
|
||||
<div class="container" hx-ext="ws" ws-connect="/heartbeat">
|
||||
<h3>WebSocket Messages</h3>
|
||||
<p>Each message just contains a random number generated by the server</p>
|
||||
<div id="idMessage">Waiting...</div>
|
||||
<div id="idMessage">Connecting...</div>
|
||||
</div>
|
||||
</main>
|
@ -1,8 +1,9 @@
|
||||
<main>
|
||||
<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>
|
||||
<a role="tab" href="/ws-heartbeat.html" aria-selected="true">Legacy Style</a>
|
||||
<a role="tab" href="/ws-heartbeat-ext.html">New Style</a>
|
||||
</div>
|
||||
|
||||
<h3>Description</h3>
|
||||
@ -16,8 +17,9 @@
|
||||
</div>
|
||||
</pre>
|
||||
|
||||
<div class="container" hx-ws="connect:ws://localhost/heartbeat">
|
||||
<div class="container" hx-ws="connect:/heartbeat">
|
||||
<h3>WebSocket Messages</h3>
|
||||
<p>Each message just contains a random number generated by the server</p>
|
||||
<div id="idMessage">Waiting...</div>
|
||||
</div>
|
||||
<div id="idMessage">Connecting...</div>
|
||||
</div>
|
||||
</main>
|
Loading…
x
Reference in New Issue
Block a user