Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions .agents/skills/jaws/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,16 @@ an outer HTTP handler to load the data and invoke a newly constructed
the page handler when initial rendering needs a Session; `AutoSession` runs at
WebSocket upgrade and is too late for initial page state.

`ui.Handler` owns `NewRequest` and exposes no Request setup hook. If a design
depends on `SetConnectFn`, either move that lifecycle into supported HTTP/session
setup or consciously build a custom page handler. A full-document Template is
not a supported workaround.
`ui.Handler` owns `NewRequest` and recognizes `jaws.ConnectHandler` in its
top-level Dot's method set, including promoted methods. It installs `JawsConnect`
before page template execution; the plain GET does not invoke it. An
implementation available only on a nested Template Dot is ignored without a
diagnostic. The bundled client connects after parsing the document. A custom
client can invoke the hook during rendering once flushed response bytes expose
the request key. Other Request setup requires a custom page handler. A
full-document Template is not a supported workaround. A connection identifies a
JaWS-capable client, not affirmative human intent; use a semantic click action
when that distinction matters.

A retained Template update keeps its wrapper Element and Jid, sends new inner
HTML, and unregisters/recreates managed descendants. It does not preserve
Expand Down
15 changes: 15 additions & 0 deletions AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,21 @@ The normal page flow has two related HTTP requests:
key, claims the pending Request through `UseRequest`, upgrades the connection,
and begins event and DOM-update processing.

When the top-level dot passed to `ui.Handler` implements `ConnectHandler`, the
handler installs its `JawsConnect` method on the Request before page template
execution. The page GET only installs the callback; an accepted WebSocket
invokes it with the `ConnectFn` lifecycle. Only the top-level dot's method set is
considered, including promoted methods. An implementation available only on a
nested `ui.Template` dot is ignored without a diagnostic. The bundled client
connects after parsing the document, while a custom client can dial once flushed
response bytes expose the request key and overlap initial template execution.
Because `ui.Handler` reuses the dot, its state and callbacks must be
concurrency-safe.

After changing state, use the exact-Element or dependency-tag scope described
above. A connection identifies a JaWS-capable client, not affirmative human
intent; use a semantic click action when that distinction matters.

`HeadHTML` does not manage response headers. The bundled client reloads pages
restored from the bfcache.

Expand Down
10 changes: 10 additions & 0 deletions contracts.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ type Updater interface {
JawsUpdate(elem *Element)
}

// ConnectHandler initializes or validates a [Request] after its WebSocket is accepted.
//
// [github.com/linkdata/jaws/lib/ui.Handler] discovers this optional capability
// only on its top-level page dot. JawsConnect has the lifecycle and permitted
// operations described by [ConnectFn].
type ConnectHandler interface {
// JawsConnect initializes or validates rq.
JawsConnect(rq *Request) error
}

// ClickHandler handles click events sent from the browser.
type ClickHandler interface {
// JawsClick is called for non-input-origin browser clicks.
Expand Down
21 changes: 18 additions & 3 deletions lib/ui/AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,16 @@ should use Go's native template action:
{{template "partial" .Dot}}
```

After creating each Request, `ui.Handler` checks the top-level Dot's method set,
including promoted methods, for `jaws.ConnectHandler` and installs `JawsConnect`
before page template execution. A plain GET only installs the callback; the
accepted WebSocket invokes it with the `jaws.ConnectFn` lifecycle. An
implementation available only on a nested Template Dot is ignored without a
diagnostic. Handler reuses its Dot across Requests, so its state and callbacks
must support concurrent execution. The bundled client connects after parsing
the document, while a custom client can dial once flushed response bytes expose
the request key and overlap initial rendering.

The Template's Dot contributes both identity and tags. It must be nil or
comparable at runtime, equal to itself, and usable under `tag.TagExpand`.
Implementing `JawsGetTag` does not repair a non-comparable Dot because tag
Expand Down Expand Up @@ -158,9 +168,14 @@ Dirty only the output that actually changed.

The bundled client forwards input, click, and context-menu events only while its
WebSocket is open and does not replay earlier interaction. When early input
matters, render controls disabled or make the region inert. Use a Request
`ConnectFn` to update a request-local readiness value and dirty its tag or the
exact Element whose updater removes the gate.
matters, render controls disabled or make the region inert. In a custom page
handler, install a Request `ConnectFn` that updates synchronized request-local
readiness and dirties the request-specific readiness tag registered by the
gate, or the exact Element whose updater removes it. A reused `ui.Handler`
shares its Dot across Requests. Its `ConnectHandler` can validate the callback
Request or update synchronized shared state, but a scalar Dot field cannot serve
as a request-local readiness gate. Ordinary tag dirtying updates matching
Elements on every live Request.

Native form reset is unsupported for managed inputs and Select. A reset button
or `form.reset()` changes browser state without the per-control events JaWS
Expand Down
50 changes: 50 additions & 0 deletions lib/ui/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,65 @@ import (
"errors"
"fmt"
"html/template"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"sync"

"github.com/linkdata/jaws"
"github.com/linkdata/jaws/lib/bind"
"github.com/linkdata/jaws/lib/tag"
"github.com/linkdata/jaws/lib/ui"
)

const exampleConnectionsHTML = `<html>
<head>{{$.HeadHTML}}</head>
<body>{{$.Span .Dot.Count}}{{$.TailHTML}}</body>
</html>`

type exampleConnections struct {
mu sync.RWMutex
count int
}

// Count returns the accepted connection count as a direct field binding.
func (state *exampleConnections) Count() bind.Binder[int] {
return bind.New(&state.mu, &state.count)
}

// JawsConnect records an accepted JaWS client connection.
func (state *exampleConnections) JawsConnect(rq *jaws.Request) error {
state.mu.Lock()
state.count++
state.mu.Unlock()
rq.Dirty(&state.count)
return nil
}

var _ jaws.ConnectHandler = (*exampleConnections)(nil)

func ExampleHandler_connectHandler() {
jw, err := jaws.New()
if err != nil {
panic(err)
}
defer jw.Close()
jw.Logger = slog.Default()

templates := template.Must(template.New("connections").Parse(exampleConnectionsHTML))
if err = jw.AddTemplateLookuper(templates); err != nil {
panic(err)
}

go jw.Serve()
mux := http.NewServeMux()
mux.Handle("GET /jaws/", jw)
mux.Handle("GET /", ui.Handler(jw, "connections", new(exampleConnections)))

_ = mux // serve mux with an HTTP server
}

type examplePathState struct {
Title string `json:"title"`
Items []string `json:"items"`
Expand Down
16 changes: 14 additions & 2 deletions lib/ui/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ func (sr *statusRecorder) WriteHeader(code int) {

func (h uiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rq := h.NewRequest(w, r)
if handler, ok := h.dot.(jaws.ConnectHandler); ok {
rq.SetConnectFn(handler.JawsConnect)
}
sr := &statusRecorder{ResponseWriter: w}
rw := RequestWriter{Request: rq, Writer: sr}
// Build a fresh per-request pointer so the UI is comparable as a map key
Expand Down Expand Up @@ -123,8 +126,17 @@ func (h uiHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// response.
//
// Handler renders without a generated wrapper and does not use dot as a tag.
// Dot may be arbitrary template data. Handler reuses dot across requests; dot
// and its callbacks must support concurrent execution.
// Dot may be arbitrary template data. When dot implements [jaws.ConnectHandler],
// Handler installs its JawsConnect method on each Request before executing the
// page template. The page GET does not invoke JawsConnect. Only the top-level
// dot's method set is considered, including promoted methods. Implementations
// available only through non-promoted fields or nested [Template] dots are
// ignored without a diagnostic.
//
// Handler reuses dot across requests, so dot and its callbacks must support
// concurrent execution. The bundled client connects after parsing the document,
// while a custom client can dial once flushed response bytes expose the request
// key and overlap the initial page render.
func Handler(jw *jaws.Jaws, name string, dot any) http.Handler {
return uiHandler{Jaws: jw, name: name, dot: dot}
}
Loading
Loading