Update reference docs: fix event names, add demos, document sx-boost target
- Remove sx:afterSettle (not dispatched), rename sx:sendError → sx:requestError - Add sx:clientRoute event (Phase 3 client-side routing) - Add working demos for all 10 events (afterRequest, afterSwap, requestError, clientRoute, sseOpen, sseMessage, sseError were missing demos) - Update sx-boost docs: configurable target selector, client routing behavior - Remove app-specific nav logic from orchestration.sx, use sx:clientRoute event - Pass page content deps to sx_response for component loading after server fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -456,14 +456,18 @@ def sx_call(component_name: str, **kwargs: Any) -> str:
|
||||
|
||||
|
||||
|
||||
def components_for_request(source: str = "") -> str:
|
||||
def components_for_request(source: str = "",
|
||||
extra_names: set[str] | None = None) -> str:
|
||||
"""Return defcomp/defmacro source for definitions the client doesn't have yet.
|
||||
|
||||
Reads the ``SX-Components`` header (comma-separated component names
|
||||
like ``~card,~nav-item``) and returns only the definitions the client
|
||||
is missing. If *source* is provided, only sends components needed
|
||||
for that source (plus transitive deps). If the header is absent,
|
||||
returns all needed defs.
|
||||
for that source (plus transitive deps).
|
||||
|
||||
*extra_names* — additional component names to include beyond what
|
||||
*source* references. Used by defpage to send components the page's
|
||||
content expression needs for client-side routing.
|
||||
"""
|
||||
from quart import request
|
||||
from .jinja_bridge import _COMPONENT_ENV
|
||||
@@ -477,6 +481,12 @@ def components_for_request(source: str = "") -> str:
|
||||
else:
|
||||
needed = None # all
|
||||
|
||||
# Merge in extra names (e.g. from page content expression deps)
|
||||
if extra_names and needed is not None:
|
||||
needed = needed | extra_names
|
||||
elif extra_names:
|
||||
needed = extra_names
|
||||
|
||||
loaded_raw = request.headers.get("SX-Components", "")
|
||||
loaded = set(loaded_raw.split(",")) if loaded_raw else set()
|
||||
|
||||
@@ -510,7 +520,8 @@ def components_for_request(source: str = "") -> str:
|
||||
|
||||
|
||||
def sx_response(source: str, status: int = 200,
|
||||
headers: dict | None = None):
|
||||
headers: dict | None = None,
|
||||
extra_component_names: set[str] | None = None):
|
||||
"""Return an s-expression wire-format response.
|
||||
|
||||
Takes a raw sx string::
|
||||
@@ -520,6 +531,10 @@ def sx_response(source: str, status: int = 200,
|
||||
For SX requests, missing component definitions are prepended as a
|
||||
``<script type="text/sx" data-components>`` block so the client
|
||||
can process them before rendering OOB content.
|
||||
|
||||
*extra_component_names* — additional component names to include beyond
|
||||
what *source* references. Used by defpage to send components the page's
|
||||
content expression needs for client-side routing.
|
||||
"""
|
||||
from quart import request, Response
|
||||
|
||||
@@ -535,7 +550,7 @@ def sx_response(source: str, status: int = 200,
|
||||
# For SX requests, prepend missing component definitions
|
||||
comp_defs = ""
|
||||
if request.headers.get("SX-Request"):
|
||||
comp_defs = components_for_request(source)
|
||||
comp_defs = components_for_request(source, extra_names=extra_component_names)
|
||||
if comp_defs:
|
||||
body = (f'<script type="text/sx" data-components>'
|
||||
f'{comp_defs}</script>\n{body}')
|
||||
|
||||
@@ -279,13 +279,25 @@ async def execute_page(
|
||||
is_htmx = is_htmx_request()
|
||||
|
||||
if is_htmx:
|
||||
# Compute content expression deps so the server sends component
|
||||
# definitions the client needs for future client-side routing
|
||||
extra_deps: set[str] | None = None
|
||||
if page_def.content_expr is not None and page_def.data_expr is None:
|
||||
from .deps import components_needed
|
||||
from .parser import serialize
|
||||
try:
|
||||
content_src = serialize(page_def.content_expr)
|
||||
extra_deps = components_needed(content_src, get_component_env())
|
||||
except Exception:
|
||||
pass # non-critical — client will just fall back to server
|
||||
|
||||
return sx_response(await oob_page_sx(
|
||||
oobs=oob_headers if oob_headers else "",
|
||||
filter=filter_sx,
|
||||
aside=aside_sx,
|
||||
content=content_sx,
|
||||
menu=menu_sx,
|
||||
))
|
||||
), extra_component_names=extra_deps)
|
||||
else:
|
||||
return await full_page_sx(
|
||||
tctx,
|
||||
|
||||
@@ -2632,7 +2632,7 @@ PLATFORM_ORCHESTRATION_JS = """
|
||||
}
|
||||
return sxRenderWithEnv(source, merged);
|
||||
} catch (e) {
|
||||
console.error("[sx-ref] sx:route eval error for:", source, e);
|
||||
logInfo("sx:route eval miss: " + (e && e.message ? e.message : e));
|
||||
return NIL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,7 +397,9 @@
|
||||
(when is-get-link
|
||||
(log-info (str "sx:route trying " (get verbInfo "url")))
|
||||
(set! client-routed
|
||||
(try-client-route (url-pathname (get verbInfo "url")))))
|
||||
(try-client-route
|
||||
(url-pathname (get verbInfo "url"))
|
||||
(dom-get-attr el "sx-target"))))
|
||||
(if client-routed
|
||||
(do
|
||||
(browser-push-state (get verbInfo "url"))
|
||||
@@ -507,48 +509,54 @@
|
||||
|
||||
(define boost-descendants
|
||||
(fn (container)
|
||||
;; Boost links and forms within a container
|
||||
;; Links get sx-get, forms get sx-post/sx-get
|
||||
(for-each
|
||||
(fn (link)
|
||||
(when (and (not (is-processed? link "boost"))
|
||||
(should-boost-link? link))
|
||||
(mark-processed! link "boost")
|
||||
;; Set default sx-target if not specified
|
||||
(when (not (dom-has-attr? link "sx-target"))
|
||||
(dom-set-attr link "sx-target" "#main-panel"))
|
||||
(when (not (dom-has-attr? link "sx-swap"))
|
||||
(dom-set-attr link "sx-swap" "innerHTML"))
|
||||
(when (not (dom-has-attr? link "sx-push-url"))
|
||||
(dom-set-attr link "sx-push-url" "true"))
|
||||
(bind-client-route-link link (dom-get-attr link "href"))))
|
||||
(dom-query-all container "a[href]"))
|
||||
(for-each
|
||||
(fn (form)
|
||||
(when (and (not (is-processed? form "boost"))
|
||||
(should-boost-form? form))
|
||||
(mark-processed! form "boost")
|
||||
(let ((method (upper (or (dom-get-attr form "method") "GET")))
|
||||
(action (or (dom-get-attr form "action")
|
||||
(browser-location-href))))
|
||||
(when (not (dom-has-attr? form "sx-target"))
|
||||
(dom-set-attr form "sx-target" "#main-panel"))
|
||||
(when (not (dom-has-attr? form "sx-swap"))
|
||||
(dom-set-attr form "sx-swap" "innerHTML"))
|
||||
(bind-boost-form form method action))))
|
||||
(dom-query-all container "form"))))
|
||||
;; Boost links and forms within a container.
|
||||
;; The sx-boost attribute value is the default target selector
|
||||
;; for boosted descendants (e.g. sx-boost="#main-panel").
|
||||
(let ((boost-target (dom-get-attr container "sx-boost")))
|
||||
(for-each
|
||||
(fn (link)
|
||||
(when (and (not (is-processed? link "boost"))
|
||||
(should-boost-link? link))
|
||||
(mark-processed! link "boost")
|
||||
;; Inherit target from boost container if not specified
|
||||
(when (and (not (dom-has-attr? link "sx-target"))
|
||||
boost-target (not (= boost-target "true")))
|
||||
(dom-set-attr link "sx-target" boost-target))
|
||||
(when (not (dom-has-attr? link "sx-swap"))
|
||||
(dom-set-attr link "sx-swap" "innerHTML"))
|
||||
(when (not (dom-has-attr? link "sx-push-url"))
|
||||
(dom-set-attr link "sx-push-url" "true"))
|
||||
(bind-client-route-link link (dom-get-attr link "href"))))
|
||||
(dom-query-all container "a[href]"))
|
||||
(for-each
|
||||
(fn (form)
|
||||
(when (and (not (is-processed? form "boost"))
|
||||
(should-boost-form? form))
|
||||
(mark-processed! form "boost")
|
||||
(let ((method (upper (or (dom-get-attr form "method") "GET")))
|
||||
(action (or (dom-get-attr form "action")
|
||||
(browser-location-href))))
|
||||
(when (and (not (dom-has-attr? form "sx-target"))
|
||||
boost-target (not (= boost-target "true")))
|
||||
(dom-set-attr form "sx-target" boost-target))
|
||||
(when (not (dom-has-attr? form "sx-swap"))
|
||||
(dom-set-attr form "sx-swap" "innerHTML"))
|
||||
(bind-boost-form form method action))))
|
||||
(dom-query-all container "form")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Client-side routing
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
;; No app-specific nav update here — apps handle sx:clientRoute event.
|
||||
|
||||
|
||||
(define try-client-route
|
||||
(fn (pathname)
|
||||
(fn (pathname target-sel)
|
||||
;; Try to render a page client-side. Returns true if successful, false otherwise.
|
||||
;; target-sel is the CSS selector for the swap target (from sx-boost value).
|
||||
;; Only works for pages without :data dependencies.
|
||||
;; Uses try-eval-content which catches errors — if a component is missing,
|
||||
;; eval fails and we fall back to server fetch transparently.
|
||||
(let ((match (find-matching-route pathname _page-routes)))
|
||||
(if (nil? match)
|
||||
(do (log-info (str "sx:route no match (" (len _page-routes) " routes) " pathname)) false)
|
||||
@@ -563,15 +571,19 @@
|
||||
(rendered (try-eval-content content-src env)))
|
||||
(if (nil? rendered)
|
||||
(do (log-info (str "sx:route server (eval failed) " pathname)) false)
|
||||
(let ((target (dom-query-by-id "main-panel")))
|
||||
(let ((target (if (and target-sel (not (= target-sel "true")))
|
||||
(dom-query target-sel)
|
||||
nil)))
|
||||
(if (nil? target)
|
||||
(do (log-warn "sx:route #main-panel not found") false)
|
||||
(do (log-warn (str "sx:route target not found: " target-sel)) false)
|
||||
(do
|
||||
(dom-set-text-content target "")
|
||||
(dom-append target rendered)
|
||||
(hoist-head-elements-full target)
|
||||
(process-elements target)
|
||||
(sx-hydrate-elements target)
|
||||
(dom-dispatch target "sx:clientRoute"
|
||||
(dict "pathname" pathname))
|
||||
(log-info (str "sx:route client " pathname))
|
||||
true))))))))))))
|
||||
|
||||
@@ -733,16 +745,22 @@
|
||||
(define handle-popstate
|
||||
(fn (scrollY)
|
||||
;; Handle browser back/forward navigation.
|
||||
;; Derive target from the nearest [sx-boost] container.
|
||||
;; Try client-side route first, fall back to server fetch.
|
||||
(let ((main (dom-query-by-id "main-panel"))
|
||||
(let ((boost-el (dom-query "[sx-boost]"))
|
||||
(url (browser-location-href)))
|
||||
(when main
|
||||
(let ((pathname (url-pathname url)))
|
||||
(if (try-client-route pathname)
|
||||
(browser-scroll-to 0 scrollY)
|
||||
(let ((headers (build-request-headers main
|
||||
(loaded-component-names) _css-hash)))
|
||||
(fetch-and-restore main url headers scrollY))))))))
|
||||
(when boost-el
|
||||
(let ((target-sel (dom-get-attr boost-el "sx-boost"))
|
||||
(target (if (and target-sel (not (= target-sel "true")))
|
||||
(dom-query target-sel)
|
||||
nil))
|
||||
(pathname (url-pathname url)))
|
||||
(when target
|
||||
(if (try-client-route pathname target-sel)
|
||||
(browser-scroll-to 0 scrollY)
|
||||
(let ((headers (build-request-headers target
|
||||
(loaded-component-names) _css-hash)))
|
||||
(fetch-and-restore target url headers scrollY)))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -795,7 +813,7 @@
|
||||
;; cross-origin
|
||||
;; success-fn: (fn (resp-ok status get-header text) ...)
|
||||
;; error-fn: (fn (err) ...)
|
||||
;; (fetch-location url) → fetch URL and swap to #main-panel
|
||||
;; (fetch-location url) → fetch URL and swap to boost target
|
||||
;; (fetch-and-restore main url headers scroll-y) → popstate fetch+swap
|
||||
;; (fetch-preload url headers cache) → preload into cache
|
||||
;;
|
||||
|
||||
Reference in New Issue
Block a user