Fix examples.sx: paren balance + dict eval crash at startup
1. Extra closing paren in ex-tabs handler 2. tab-content dict values contained (div ...) HTML tags which crash during register_components since HTML primitives aren't in env Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -561,3 +561,15 @@ def prim_into(target: Any, coll: Any) -> Any:
|
||||
return result
|
||||
raise ValueError(f"into: unsupported target type {type(target).__name__}")
|
||||
|
||||
|
||||
@register_primitive("random-int")
|
||||
def prim_random_int(low: int, high: int) -> int:
|
||||
import random
|
||||
return random.randint(int(low), int(high))
|
||||
|
||||
|
||||
@register_primitive("json-encode")
|
||||
def prim_json_encode(value) -> str:
|
||||
import json
|
||||
return json.dumps(value, indent=2)
|
||||
|
||||
|
||||
@@ -408,6 +408,18 @@ async def _io_request_form_all(
|
||||
return dict(form)
|
||||
|
||||
|
||||
@register_io_handler("request-form-list")
|
||||
async def _io_request_form_list(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
) -> list:
|
||||
"""``(request-form-list "field")`` → all values for a multi-value form field."""
|
||||
if not args:
|
||||
raise ValueError("request-form-list requires a field name")
|
||||
from quart import request
|
||||
form = await request.form
|
||||
return form.getlist(str(args[0]))
|
||||
|
||||
|
||||
@register_io_handler("request-headers-all")
|
||||
async def _io_request_headers_all(
|
||||
args: list[Any], kwargs: dict[str, Any], ctx: RequestContext
|
||||
|
||||
@@ -202,6 +202,13 @@
|
||||
:doc "All form fields as a dict."
|
||||
:context :request)
|
||||
|
||||
(define-io-primitive "request-form-list"
|
||||
:params (field-name)
|
||||
:returns "list"
|
||||
:async true
|
||||
:doc "All values for a multi-value form field as a list."
|
||||
:context :request)
|
||||
|
||||
(define-io-primitive "request-headers-all"
|
||||
:params ()
|
||||
:returns "dict"
|
||||
|
||||
@@ -70,6 +70,17 @@
|
||||
:doc "Modulo a % b."
|
||||
:body (native-mod a b))
|
||||
|
||||
(define-primitive "random-int"
|
||||
:params ((low :as number) (high :as number))
|
||||
:returns "number"
|
||||
:doc "Random integer in [low, high] inclusive."
|
||||
:body (native-random-int low high))
|
||||
|
||||
(define-primitive "json-encode"
|
||||
:params (value)
|
||||
:returns "string"
|
||||
:doc "Encode value as JSON string with indentation.")
|
||||
|
||||
(define-primitive "sqrt"
|
||||
:params ((x :as number))
|
||||
:returns "number"
|
||||
|
||||
@@ -1,714 +1,21 @@
|
||||
"""SX docs page routes.
|
||||
|
||||
Page GET routes are defined declaratively in sxc/pages/docs.sx via defpage.
|
||||
This file contains only redirect routes and example API endpoints.
|
||||
Example API endpoints are now defined in sx/handlers/examples.sx via defhandler.
|
||||
This file contains only SSE and marsh demo endpoints that need Python.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import random
|
||||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
from quart import Blueprint, Response, make_response, request
|
||||
from shared.browser.app.csrf import csrf_exempt
|
||||
from quart import Blueprint, Response, request
|
||||
|
||||
|
||||
def register(url_prefix: str = "/") -> Blueprint:
|
||||
bp = Blueprint("pages", __name__, url_prefix=url_prefix)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Example API endpoints (for live demos)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/click")
|
||||
async def api_click():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
sx_src = f'(~click-result :time "{now}")'
|
||||
comp_text = _component_source_text("click-result")
|
||||
wire_text = _full_wire_text(sx_src, "click-result")
|
||||
oob_wire = _oob_code("click-wire", wire_text)
|
||||
oob_comp = _oob_code("click-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/geography/hypermedia/examples/api/form")
|
||||
async def api_form():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
name = form.get("name", "")
|
||||
escaped = name.replace('"', '\\"')
|
||||
sx_src = f'(~form-result :name "{escaped}")'
|
||||
comp_text = _component_source_text("form-result")
|
||||
wire_text = _full_wire_text(sx_src, "form-result")
|
||||
oob_wire = _oob_code("form-wire", wire_text)
|
||||
oob_comp = _oob_code("form-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
_poll_count = {"n": 0}
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/poll")
|
||||
async def api_poll():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
_poll_count["n"] += 1
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
count = min(_poll_count["n"], 10)
|
||||
sx_src = f'(~poll-result :time "{now}" :count {count})'
|
||||
comp_text = _component_source_text("poll-result")
|
||||
wire_text = _full_wire_text(sx_src, "poll-result")
|
||||
oob_wire = _oob_code("poll-wire", wire_text)
|
||||
oob_comp = _oob_code("poll-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.delete("/geography/hypermedia/examples/api/delete/<item_id>")
|
||||
async def api_delete(item_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
# Empty primary response — outerHTML swap removes the row
|
||||
# But send OOB swaps to show what happened
|
||||
wire_text = _full_wire_text(f'(empty — row #{item_id} removed by outerHTML swap)')
|
||||
comp_text = _component_source_text("delete-row")
|
||||
oob_wire = _oob_code("delete-wire", wire_text)
|
||||
oob_comp = _oob_code("delete-comp", comp_text)
|
||||
return sx_response(f'(<> {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/edit")
|
||||
async def api_edit_form():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
value = request.args.get("value", "")
|
||||
escaped = value.replace('"', '\\"')
|
||||
sx_src = f'(~inline-edit-form :value "{escaped}")'
|
||||
comp_text = _component_source_text("inline-edit-form")
|
||||
wire_text = _full_wire_text(sx_src, "inline-edit-form")
|
||||
oob_wire = _oob_code("edit-wire", wire_text)
|
||||
oob_comp = _oob_code("edit-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/geography/hypermedia/examples/api/edit")
|
||||
async def api_edit_save():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
value = form.get("value", "")
|
||||
escaped = value.replace('"', '\\"')
|
||||
sx_src = f'(~inline-view :value "{escaped}")'
|
||||
comp_text = _component_source_text("inline-view")
|
||||
wire_text = _full_wire_text(sx_src, "inline-view")
|
||||
oob_wire = _oob_code("edit-wire", wire_text)
|
||||
oob_comp = _oob_code("edit-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/edit/cancel")
|
||||
async def api_edit_cancel():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
value = request.args.get("value", "")
|
||||
escaped = value.replace('"', '\\"')
|
||||
sx_src = f'(~inline-view :value "{escaped}")'
|
||||
comp_text = _component_source_text("inline-view")
|
||||
wire_text = _full_wire_text(sx_src, "inline-view")
|
||||
oob_wire = _oob_code("edit-wire", wire_text)
|
||||
oob_comp = _oob_code("edit-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/oob")
|
||||
async def api_oob():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = (
|
||||
f'(<>'
|
||||
f' (p :class "text-emerald-600 font-medium" "Box A updated!")'
|
||||
f' (p :class "text-sm text-stone-500" "at {now}")'
|
||||
f' (div :id "oob-box-b" :sx-swap-oob "innerHTML"'
|
||||
f' (p :class "text-violet-600 font-medium" "Box B updated via OOB!")'
|
||||
f' (p :class "text-sm text-stone-500" "at {now}")))'
|
||||
)
|
||||
wire_text = _full_wire_text(sx_src)
|
||||
oob_wire = _oob_code("oob-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire})')
|
||||
|
||||
# --- Lazy Loading ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/lazy")
|
||||
async def api_lazy():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~lazy-result :time "{now}")'
|
||||
comp_text = _component_source_text("lazy-result")
|
||||
wire_text = _full_wire_text(sx_src, "lazy-result")
|
||||
oob_wire = _oob_code("lazy-wire", wire_text)
|
||||
oob_comp = _oob_code("lazy-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Infinite Scroll ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/scroll")
|
||||
async def api_scroll():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
page = int(request.args.get("page", 2))
|
||||
start = (page - 1) * 5 + 1
|
||||
next_page = page + 1
|
||||
items_html = " ".join(
|
||||
f'(div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700" "Item {i} — loaded from page {page}")'
|
||||
for i in range(start, start + 5)
|
||||
)
|
||||
if next_page <= 6:
|
||||
sentinel = (
|
||||
f'(div :id "scroll-sentinel"'
|
||||
f' :sx-get "/geography/hypermedia/examples/api/scroll?page={next_page}"'
|
||||
f' :sx-trigger "intersect once"'
|
||||
f' :sx-target "#scroll-items"'
|
||||
f' :sx-swap "beforeend"'
|
||||
f' :class "p-3 text-center text-stone-400 text-sm"'
|
||||
f' "Loading more...")'
|
||||
)
|
||||
else:
|
||||
sentinel = (
|
||||
'(div :class "p-3 text-center text-stone-500 text-sm font-medium"'
|
||||
' "All items loaded.")'
|
||||
)
|
||||
sx_src = f'(<> {items_html} {sentinel})'
|
||||
wire_text = _full_wire_text(sx_src)
|
||||
oob_wire = _oob_code("scroll-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire})')
|
||||
|
||||
# --- Progress Bar ---
|
||||
|
||||
_jobs: dict[str, int] = {}
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/geography/hypermedia/examples/api/progress/start")
|
||||
async def api_progress_start():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
job_id = str(uuid4())[:8]
|
||||
_jobs[job_id] = 0
|
||||
sx_src = f'(~progress-status :percent 0 :job-id "{job_id}")'
|
||||
comp_text = _component_source_text("progress-status")
|
||||
wire_text = _full_wire_text(sx_src, "progress-status")
|
||||
oob_wire = _oob_code("progress-wire", wire_text)
|
||||
oob_comp = _oob_code("progress-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/progress/status")
|
||||
async def api_progress_status():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
job_id = request.args.get("job", "")
|
||||
current = _jobs.get(job_id, 0)
|
||||
current = min(current + random.randint(15, 30), 100)
|
||||
_jobs[job_id] = current
|
||||
sx_src = f'(~progress-status :percent {current} :job-id "{job_id}")'
|
||||
comp_text = _component_source_text("progress-status")
|
||||
wire_text = _full_wire_text(sx_src, "progress-status")
|
||||
oob_wire = _oob_code("progress-wire", wire_text)
|
||||
oob_comp = _oob_code("progress-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Active Search ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/search")
|
||||
async def api_search():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
from content.pages import SEARCH_LANGUAGES
|
||||
q = request.args.get("q", "").strip().lower()
|
||||
if not q:
|
||||
results = SEARCH_LANGUAGES
|
||||
else:
|
||||
results = [lang for lang in SEARCH_LANGUAGES if q in lang.lower()]
|
||||
items_sx = " ".join(f'"{r}"' for r in results)
|
||||
escaped_q = q.replace('"', '\\"')
|
||||
sx_src = f'(~search-results :items (list {items_sx}) :query "{escaped_q}")'
|
||||
comp_text = _component_source_text("search-results")
|
||||
wire_text = _full_wire_text(sx_src, "search-results")
|
||||
oob_wire = _oob_code("search-wire", wire_text)
|
||||
oob_comp = _oob_code("search-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Inline Validation ---
|
||||
|
||||
_TAKEN_EMAILS = {"admin@example.com", "test@example.com", "user@example.com"}
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/validate")
|
||||
async def api_validate():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
email = request.args.get("email", "").strip()
|
||||
if not email:
|
||||
sx_src = '(~validation-error :message "Email is required")'
|
||||
comp_name = "validation-error"
|
||||
elif "@" not in email or "." not in email.split("@")[-1]:
|
||||
sx_src = '(~validation-error :message "Invalid email format")'
|
||||
comp_name = "validation-error"
|
||||
elif email.lower() in _TAKEN_EMAILS:
|
||||
escaped = email.replace('"', '\\"')
|
||||
sx_src = f'(~validation-error :message "{escaped} is already taken")'
|
||||
comp_name = "validation-error"
|
||||
else:
|
||||
escaped = email.replace('"', '\\"')
|
||||
sx_src = f'(~validation-ok :email "{escaped}")'
|
||||
comp_name = "validation-ok"
|
||||
comp_text = _component_source_text(comp_name)
|
||||
wire_text = _full_wire_text(sx_src, comp_name)
|
||||
oob_wire = _oob_code("validate-wire", wire_text)
|
||||
oob_comp = _oob_code("validate-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/geography/hypermedia/examples/api/validate/submit")
|
||||
async def api_validate_submit():
|
||||
from shared.sx.helpers import sx_response
|
||||
form = await request.form
|
||||
email = form.get("email", "").strip()
|
||||
if not email or "@" not in email:
|
||||
return sx_response('(p :class "text-sm text-rose-600 mt-2" "Please enter a valid email.")')
|
||||
escaped = email.replace('"', '\\"')
|
||||
return sx_response(f'(p :class "text-sm text-emerald-600 mt-2" "Form submitted with: {escaped}")')
|
||||
|
||||
# --- Value Select ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/values")
|
||||
async def api_values():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
from content.pages import VALUE_SELECT_DATA
|
||||
cat = request.args.get("category", "")
|
||||
items = VALUE_SELECT_DATA.get(cat, [])
|
||||
options_sx = " ".join(f'(option :value "{i}" "{i}")' for i in items)
|
||||
if not options_sx:
|
||||
options_sx = '(option :value "" "No items")'
|
||||
sx_src = f'(<> {options_sx})'
|
||||
wire_text = _full_wire_text(sx_src)
|
||||
oob_wire = _oob_code("values-wire", wire_text)
|
||||
return sx_response(f'(<> {options_sx} {oob_wire})')
|
||||
|
||||
# --- Reset on Submit ---
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/geography/hypermedia/examples/api/reset-submit")
|
||||
async def api_reset_submit():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
msg = form.get("message", "").strip() or "(empty)"
|
||||
escaped = msg.replace('"', '\\"')
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~reset-message :message "{escaped}" :time "{now}")'
|
||||
comp_text = _component_source_text("reset-message")
|
||||
wire_text = _full_wire_text(sx_src, "reset-message")
|
||||
oob_wire = _oob_code("reset-wire", wire_text)
|
||||
oob_comp = _oob_code("reset-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Edit Row ---
|
||||
|
||||
_edit_rows: dict[str, dict] = {}
|
||||
|
||||
def _get_edit_rows() -> dict[str, dict]:
|
||||
if not _edit_rows:
|
||||
from content.pages import EDIT_ROW_DATA
|
||||
for r in EDIT_ROW_DATA:
|
||||
_edit_rows[r["id"]] = dict(r)
|
||||
return _edit_rows
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/editrow/<row_id>")
|
||||
async def api_editrow_form(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
rows = _get_edit_rows()
|
||||
row = rows.get(row_id, {"id": row_id, "name": "", "price": "0", "stock": "0"})
|
||||
sx_src = (f'(~edit-row-form :id "{row["id"]}" :name "{row["name"]}"'
|
||||
f' :price "{row["price"]}" :stock "{row["stock"]}")')
|
||||
comp_text = _component_source_text("edit-row-form")
|
||||
wire_text = _full_wire_text(sx_src, "edit-row-form")
|
||||
oob_wire = _oob_code("editrow-wire", wire_text)
|
||||
oob_comp = _oob_code("editrow-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/geography/hypermedia/examples/api/editrow/<row_id>")
|
||||
async def api_editrow_save(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
rows = _get_edit_rows()
|
||||
rows[row_id] = {
|
||||
"id": row_id,
|
||||
"name": form.get("name", ""),
|
||||
"price": form.get("price", "0"),
|
||||
"stock": form.get("stock", "0"),
|
||||
}
|
||||
row = rows[row_id]
|
||||
sx_src = (f'(~edit-row-view :id "{row["id"]}" :name "{row["name"]}"'
|
||||
f' :price "{row["price"]}" :stock "{row["stock"]}")')
|
||||
comp_text = _component_source_text("edit-row-view")
|
||||
wire_text = _full_wire_text(sx_src, "edit-row-view")
|
||||
oob_wire = _oob_code("editrow-wire", wire_text)
|
||||
oob_comp = _oob_code("editrow-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/editrow/<row_id>/cancel")
|
||||
async def api_editrow_cancel(row_id: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
rows = _get_edit_rows()
|
||||
row = rows.get(row_id, {"id": row_id, "name": "", "price": "0", "stock": "0"})
|
||||
sx_src = (f'(~edit-row-view :id "{row["id"]}" :name "{row["name"]}"'
|
||||
f' :price "{row["price"]}" :stock "{row["stock"]}")')
|
||||
comp_text = _component_source_text("edit-row-view")
|
||||
wire_text = _full_wire_text(sx_src, "edit-row-view")
|
||||
oob_wire = _oob_code("editrow-wire", wire_text)
|
||||
oob_comp = _oob_code("editrow-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Bulk Update ---
|
||||
|
||||
_bulk_users: dict[str, dict] = {}
|
||||
|
||||
def _get_bulk_users() -> dict[str, dict]:
|
||||
if not _bulk_users:
|
||||
from content.pages import BULK_USERS
|
||||
for u in BULK_USERS:
|
||||
_bulk_users[u["id"]] = dict(u)
|
||||
return _bulk_users
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/geography/hypermedia/examples/api/bulk")
|
||||
async def api_bulk():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
action = request.args.get("action", "activate")
|
||||
form = await request.form
|
||||
ids = form.getlist("ids")
|
||||
users = _get_bulk_users()
|
||||
new_status = "active" if action == "activate" else "inactive"
|
||||
for uid in ids:
|
||||
if uid in users:
|
||||
users[uid]["status"] = new_status
|
||||
rows = []
|
||||
for u in users.values():
|
||||
rows.append(
|
||||
f'(~bulk-row :id "{u["id"]}" :name "{u["name"]}"'
|
||||
f' :email "{u["email"]}" :status "{u["status"]}")'
|
||||
)
|
||||
sx_src = f'(<> {" ".join(rows)})'
|
||||
comp_text = _component_source_text("bulk-row")
|
||||
wire_text = _full_wire_text(sx_src, "bulk-row")
|
||||
oob_wire = _oob_code("bulk-wire", wire_text)
|
||||
oob_comp = _oob_code("bulk-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Swap Positions ---
|
||||
|
||||
_swap_count = {"n": 0}
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/geography/hypermedia/examples/api/swap-log")
|
||||
async def api_swap_log():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
mode = request.args.get("mode", "beforeend")
|
||||
_swap_count["n"] += 1
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
n = _swap_count["n"]
|
||||
entry = f'(div :class "px-3 py-2 text-sm text-stone-700" "[{now}] {mode} (#{n})")'
|
||||
oob_counter = (
|
||||
f'(span :id "swap-counter" :sx-swap-oob "innerHTML"'
|
||||
f' :class "self-center text-sm text-stone-500" "Count: {n}")'
|
||||
)
|
||||
sx_src = f'(<> {entry} {oob_counter})'
|
||||
wire_text = _full_wire_text(sx_src)
|
||||
oob_wire = _oob_code("swap-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire})')
|
||||
|
||||
# --- Select Filter (dashboard) ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/dashboard")
|
||||
async def api_dashboard():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = (
|
||||
f'(<>'
|
||||
f' (div :id "dash-header" :class "p-3 bg-violet-50 rounded mb-3"'
|
||||
f' (h4 :class "font-semibold text-violet-800" "Dashboard Header")'
|
||||
f' (p :class "text-sm text-violet-600" "Generated at {now}"))'
|
||||
f' (div :id "dash-stats" :class "grid grid-cols-3 gap-3 mb-3"'
|
||||
f' (div :class "p-3 bg-emerald-50 rounded text-center"'
|
||||
f' (p :class "text-2xl font-bold text-emerald-700" "142")'
|
||||
f' (p :class "text-xs text-emerald-600" "Users"))'
|
||||
f' (div :class "p-3 bg-blue-50 rounded text-center"'
|
||||
f' (p :class "text-2xl font-bold text-blue-700" "89")'
|
||||
f' (p :class "text-xs text-blue-600" "Orders"))'
|
||||
f' (div :class "p-3 bg-amber-50 rounded text-center"'
|
||||
f' (p :class "text-2xl font-bold text-amber-700" "$4.2k")'
|
||||
f' (p :class "text-xs text-amber-600" "Revenue")))'
|
||||
f' (div :id "dash-footer" :class "p-3 bg-stone-50 rounded"'
|
||||
f' (p :class "text-sm text-stone-500" "Last updated: {now}")))'
|
||||
)
|
||||
wire_text = _full_wire_text(sx_src)
|
||||
oob_wire = _oob_code("filter-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire})')
|
||||
|
||||
# --- Tabs ---
|
||||
|
||||
_TAB_CONTENT = {
|
||||
"tab1": ('(div (p :class "text-stone-700" "Welcome to the Overview tab.")'
|
||||
' (p :class "text-stone-500 text-sm mt-2"'
|
||||
' "This is the default tab content loaded via sx-get."))'),
|
||||
"tab2": ('(div (p :class "text-stone-700" "Here are the details.")'
|
||||
' (ul :class "mt-2 space-y-1 text-sm text-stone-600"'
|
||||
' (li "Version: 1.0.0")'
|
||||
' (li "Build: 2024-01-15")'
|
||||
' (li "Engine: sx")))'),
|
||||
"tab3": ('(div (p :class "text-stone-700" "Recent history:")'
|
||||
' (ol :class "mt-2 space-y-1 text-sm text-stone-600 list-decimal list-inside"'
|
||||
' (li "Initial release")'
|
||||
' (li "Added component caching")'
|
||||
' (li "Wire format v2")))'),
|
||||
}
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/tabs/<tab>")
|
||||
async def api_tabs(tab: str):
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
sx_src = _TAB_CONTENT.get(tab, _TAB_CONTENT["tab1"])
|
||||
buttons = []
|
||||
for t, label in [("tab1", "Overview"), ("tab2", "Details"), ("tab3", "History")]:
|
||||
active = "true" if t == tab else "false"
|
||||
buttons.append(f'(~tab-btn :tab "{t}" :label "{label}" :active "{active}")')
|
||||
oob_tabs = (
|
||||
f'(div :id "tab-buttons" :sx-swap-oob "innerHTML"'
|
||||
f' :class "flex border-b border-stone-200"'
|
||||
f' {" ".join(buttons)})'
|
||||
)
|
||||
wire_text = _full_wire_text(f'(<> {sx_src} {oob_tabs})', "tab-btn")
|
||||
oob_wire = _oob_code("tabs-wire", wire_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_tabs} {oob_wire})')
|
||||
|
||||
# --- Animations ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/animate")
|
||||
async def api_animate():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
colors = ["bg-violet-100", "bg-emerald-100", "bg-blue-100", "bg-amber-100", "bg-rose-100"]
|
||||
color = random.choice(colors)
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~anim-result :color "{color}" :time "{now}")'
|
||||
comp_text = _component_source_text("anim-result")
|
||||
wire_text = _full_wire_text(sx_src, "anim-result")
|
||||
oob_wire = _oob_code("anim-wire", wire_text)
|
||||
oob_comp = _oob_code("anim-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Dialogs ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/dialog")
|
||||
async def api_dialog():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
sx_src = '(~dialog-modal :title "Confirm Action" :message "Are you sure you want to proceed? This is a demo dialog rendered entirely with sx components.")'
|
||||
comp_text = _component_source_text("dialog-modal")
|
||||
wire_text = _full_wire_text(sx_src, "dialog-modal")
|
||||
oob_wire = _oob_code("dialog-wire", wire_text)
|
||||
oob_comp = _oob_code("dialog-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/dialog/close")
|
||||
async def api_dialog_close():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _full_wire_text
|
||||
wire_text = _full_wire_text("(empty — dialog closed)")
|
||||
oob_wire = _oob_code("dialog-wire", wire_text)
|
||||
return sx_response(f'(<> {oob_wire})')
|
||||
|
||||
# --- Keyboard Shortcuts ---
|
||||
|
||||
_KBD_ACTIONS = {
|
||||
"s": "Search panel activated",
|
||||
"n": "New item created",
|
||||
"h": "Help panel opened",
|
||||
}
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/keyboard")
|
||||
async def api_keyboard():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
key = request.args.get("key", "")
|
||||
action = _KBD_ACTIONS.get(key, f"Unknown key: {key}")
|
||||
escaped_action = action.replace('"', '\\"')
|
||||
escaped_key = key.replace('"', '\\"')
|
||||
sx_src = f'(~kbd-result :key "{escaped_key}" :action "{escaped_action}")'
|
||||
comp_text = _component_source_text("kbd-result")
|
||||
wire_text = _full_wire_text(sx_src, "kbd-result")
|
||||
oob_wire = _oob_code("kbd-wire", wire_text)
|
||||
oob_comp = _oob_code("kbd-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- PUT / PATCH ---
|
||||
|
||||
_profile = {}
|
||||
|
||||
def _get_profile() -> dict:
|
||||
if not _profile:
|
||||
from content.pages import PROFILE_DEFAULT
|
||||
_profile.update(PROFILE_DEFAULT)
|
||||
return _profile
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/putpatch/edit-all")
|
||||
async def api_pp_edit_all():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
p = _get_profile()
|
||||
sx_src = f'(~pp-form-full :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
|
||||
comp_text = _component_source_text("pp-form-full")
|
||||
wire_text = _full_wire_text(sx_src, "pp-form-full")
|
||||
oob_wire = _oob_code("pp-wire", wire_text)
|
||||
oob_comp = _oob_code("pp-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@csrf_exempt
|
||||
@bp.put("/geography/hypermedia/examples/api/putpatch")
|
||||
async def api_pp_put():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
p = _get_profile()
|
||||
p["name"] = form.get("name", p["name"])
|
||||
p["email"] = form.get("email", p["email"])
|
||||
p["role"] = form.get("role", p["role"])
|
||||
sx_src = f'(~pp-view :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
|
||||
comp_text = _component_source_text("pp-view")
|
||||
wire_text = _full_wire_text(sx_src, "pp-view")
|
||||
oob_wire = _oob_code("pp-wire", wire_text)
|
||||
oob_comp = _oob_code("pp-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/putpatch/cancel")
|
||||
async def api_pp_cancel():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
p = _get_profile()
|
||||
sx_src = f'(~pp-view :name "{p["name"]}" :email "{p["email"]}" :role "{p["role"]}")'
|
||||
comp_text = _component_source_text("pp-view")
|
||||
wire_text = _full_wire_text(sx_src, "pp-view")
|
||||
oob_wire = _oob_code("pp-wire", wire_text)
|
||||
oob_comp = _oob_code("pp-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- JSON Encoding ---
|
||||
|
||||
@csrf_exempt
|
||||
@bp.post("/geography/hypermedia/examples/api/json-echo")
|
||||
async def api_json_echo():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
data = await request.get_json(silent=True) or {}
|
||||
body = json.dumps(data, indent=2)
|
||||
ct = request.content_type or "unknown"
|
||||
escaped_body = body.replace('\\', '\\\\').replace('"', '\\"')
|
||||
escaped_ct = ct.replace('"', '\\"')
|
||||
sx_src = f'(~json-result :body "{escaped_body}" :content-type "{escaped_ct}")'
|
||||
comp_text = _component_source_text("json-result")
|
||||
wire_text = _full_wire_text(sx_src, "json-result")
|
||||
oob_wire = _oob_code("json-wire", wire_text)
|
||||
oob_comp = _oob_code("json-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Vals & Headers ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/echo-vals")
|
||||
async def api_echo_vals():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
vals = {k: v for k, v in request.args.items()
|
||||
if k not in ("_", "sx-request")}
|
||||
items_sx = " ".join(f'"{k}: {v}"' for k, v in vals.items())
|
||||
sx_src = f'(~echo-result :label "values" :items (list {items_sx}))'
|
||||
comp_text = _component_source_text("echo-result")
|
||||
wire_text = _full_wire_text(sx_src, "echo-result")
|
||||
oob_wire = _oob_code("vals-wire", wire_text)
|
||||
oob_comp = _oob_code("vals-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/echo-headers")
|
||||
async def api_echo_headers():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
custom = {k: v for k, v in request.headers if k.lower().startswith("x-")}
|
||||
items_sx = " ".join(f'"{k}: {v}"' for k, v in custom.items())
|
||||
sx_src = f'(~echo-result :label "headers" :items (list {items_sx}))'
|
||||
comp_text = _component_source_text("echo-result")
|
||||
wire_text = _full_wire_text(sx_src, "echo-result")
|
||||
oob_wire = _oob_code("vals-wire", wire_text)
|
||||
oob_comp = _oob_code("vals-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Loading States ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/slow")
|
||||
async def api_slow():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
await asyncio.sleep(2)
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
sx_src = f'(~loading-result :time "{now}")'
|
||||
comp_text = _component_source_text("loading-result")
|
||||
wire_text = _full_wire_text(sx_src, "loading-result")
|
||||
oob_wire = _oob_code("loading-wire", wire_text)
|
||||
oob_comp = _oob_code("loading-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Request Abort (sync replace) ---
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/slow-search")
|
||||
async def api_slow_search():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
delay = random.uniform(0.5, 2.0)
|
||||
await asyncio.sleep(delay)
|
||||
q = request.args.get("q", "").strip()
|
||||
delay_ms = int(delay * 1000)
|
||||
escaped = q.replace('"', '\\"')
|
||||
sx_src = f'(~sync-result :query "{escaped}" :delay "{delay_ms}")'
|
||||
comp_text = _component_source_text("sync-result")
|
||||
wire_text = _full_wire_text(sx_src, "sync-result")
|
||||
oob_wire = _oob_code("sync-wire", wire_text)
|
||||
oob_comp = _oob_code("sync-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# --- Retry ---
|
||||
|
||||
_flaky = {"n": 0}
|
||||
|
||||
@bp.get("/geography/hypermedia/examples/api/flaky")
|
||||
async def api_flaky():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.pages.renders import _oob_code, _component_source_text, _full_wire_text
|
||||
_flaky["n"] += 1
|
||||
n = _flaky["n"]
|
||||
if n % 3 != 0:
|
||||
return Response("", status=503, content_type="text/plain")
|
||||
sx_src = f'(~retry-result :attempt "{n}" :message "Success! The endpoint finally responded.")'
|
||||
comp_text = _component_source_text("retry-result")
|
||||
wire_text = _full_wire_text(sx_src, "retry-result")
|
||||
oob_wire = _oob_code("retry-wire", wire_text)
|
||||
oob_comp = _oob_code("retry-comp", comp_text)
|
||||
return sx_response(f'(<> {sx_src} {oob_wire} {oob_comp})')
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reference API endpoints — remaining Python-only
|
||||
#
|
||||
|
||||
@@ -114,3 +114,8 @@
|
||||
:params (filename title desc)
|
||||
:returns "dict"
|
||||
:service "sx")
|
||||
|
||||
(define-page-helper "handler-source"
|
||||
:params (name)
|
||||
:returns "string"
|
||||
:service "sx")
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
:demo-description "Click the button to load server-rendered content."
|
||||
:demo (~click-to-load-demo)
|
||||
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/click\"\n :sx-target \"#click-result\"\n :sx-swap \"innerHTML\"\n \"Load content\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/click\")\nasync def api_click():\n now = datetime.now().strftime(...)\n return sx_response(\n f'(~click-result :time \"{now}\")')"
|
||||
:handler-code (handler-source "ex-click")
|
||||
:comp-placeholder-id "click-comp"
|
||||
:wire-placeholder-id "click-wire"
|
||||
:wire-note "The server responds with content-type text/sx. New CSS rules are prepended as a style tag. Clear the component cache to see component definitions included in the wire response."))
|
||||
@@ -21,7 +21,7 @@
|
||||
:demo-description "Enter a name and submit."
|
||||
:demo (~form-demo)
|
||||
:sx-code "(form\n :sx-post \"/geography/hypermedia/examples/api/form\"\n :sx-target \"#form-result\"\n :sx-swap \"innerHTML\"\n (input :type \"text\" :name \"name\")\n (button :type \"submit\" \"Submit\"))"
|
||||
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/form\")\nasync def api_form():\n form = await request.form\n name = form.get(\"name\", \"\")\n return sx_response(\n f'(~form-result :name \"{name}\")')"
|
||||
:handler-code (handler-source "ex-form")
|
||||
:comp-placeholder-id "form-comp"
|
||||
:wire-placeholder-id "form-wire"))
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
:demo-description "This div polls the server every 2 seconds."
|
||||
:demo (~polling-demo)
|
||||
:sx-code "(div\n :sx-get \"/geography/hypermedia/examples/api/poll\"\n :sx-trigger \"load, every 2s\"\n :sx-swap \"innerHTML\"\n \"Loading...\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/poll\")\nasync def api_poll():\n poll_count[\"n\"] += 1\n now = datetime.now().strftime(\"%H:%M:%S\")\n count = min(poll_count[\"n\"], 10)\n return sx_response(\n f'(~poll-result :time \"{now}\" :count {count})')"
|
||||
:handler-code (handler-source "ex-poll")
|
||||
:comp-placeholder-id "poll-comp"
|
||||
:wire-placeholder-id "poll-wire"
|
||||
:wire-note "Updates every 2 seconds — watch the time and count change."))
|
||||
@@ -49,7 +49,7 @@
|
||||
(list "4" "Deploy to production")
|
||||
(list "5" "Add unit tests")))
|
||||
:sx-code "(button\n :sx-delete \"/api/delete/1\"\n :sx-target \"#row-1\"\n :sx-swap \"outerHTML\"\n :sx-confirm \"Delete this item?\"\n \"delete\")"
|
||||
:handler-code "@bp.delete(\"/geography/hypermedia/examples/api/delete/<item_id>\")\nasync def api_delete(item_id: str):\n # Empty response — outerHTML swap removes the row\n return Response(\"\", status=200,\n content_type=\"text/sx\")"
|
||||
:handler-code (handler-source "ex-delete")
|
||||
:comp-placeholder-id "delete-comp"
|
||||
:wire-placeholder-id "delete-wire"
|
||||
:wire-note "Empty body — outerHTML swap replaces the target element with nothing."))
|
||||
@@ -61,7 +61,7 @@
|
||||
:demo-description "Click edit, modify the text, save or cancel."
|
||||
:demo (~inline-edit-demo)
|
||||
:sx-code ";; View mode — shows text + edit button\n(~inline-view :value \"some text\")\n\n;; Edit mode — returned by server on click\n(~inline-edit-form :value \"some text\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/edit\")\nasync def api_edit_form():\n value = request.args.get(\"value\", \"\")\n return sx_response(\n f'(~inline-edit-form :value \"{value}\")')\n\n@bp.post(\"/geography/hypermedia/examples/api/edit\")\nasync def api_edit_save():\n form = await request.form\n value = form.get(\"value\", \"\")\n return sx_response(\n f'(~inline-view :value \"{value}\")')"
|
||||
:handler-code (str (handler-source "ex-edit-form") "\n\n" (handler-source "ex-edit-save"))
|
||||
:comp-placeholder-id "edit-comp"
|
||||
:comp-heading "Components"
|
||||
:handler-heading "Server handlers"
|
||||
@@ -74,7 +74,7 @@
|
||||
:demo-description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."
|
||||
:demo (~oob-demo)
|
||||
:sx-code ";; Button targets Box A\n(button\n :sx-get \"/geography/hypermedia/examples/api/oob\"\n :sx-target \"#oob-box-a\"\n :sx-swap \"innerHTML\"\n \"Update both boxes\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/oob\")\nasync def api_oob():\n now = datetime.now().strftime(\"%H:%M:%S\")\n return sx_response(\n f'(<>'\n f' (p \"Box A updated at {now}\")'\n f' (div :id \"oob-box-b\"'\n f' :sx-swap-oob \"innerHTML\"'\n f' (p \"Box B updated at {now}\")))')"
|
||||
:handler-code (handler-source "ex-oob")
|
||||
:wire-placeholder-id "oob-wire"
|
||||
:wire-note "The fragment contains both the main content and an OOB element. sx.js splits them: main content goes to sx-target, OOB elements find their targets by ID."))
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
:demo-description "Content loads automatically when the page renders."
|
||||
:demo (~lazy-loading-demo)
|
||||
:sx-code "(div\n :sx-get \"/geography/hypermedia/examples/api/lazy\"\n :sx-trigger \"load\"\n :sx-swap \"innerHTML\"\n (div :class \"animate-pulse\" \"Loading...\"))"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/lazy\")\nasync def api_lazy():\n now = datetime.now().strftime(...)\n return sx_response(\n f'(~lazy-result :time \"{now}\")')"
|
||||
:handler-code (handler-source "ex-lazy")
|
||||
:comp-placeholder-id "lazy-comp"
|
||||
:wire-placeholder-id "lazy-wire"))
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
:demo-description "Scroll down in the container to load more items (5 pages total)."
|
||||
:demo (~infinite-scroll-demo)
|
||||
:sx-code "(div :id \"scroll-sentinel\"\n :sx-get \"/geography/hypermedia/examples/api/scroll?page=2\"\n :sx-trigger \"intersect once\"\n :sx-target \"#scroll-items\"\n :sx-swap \"beforeend\"\n \"Loading more...\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/scroll\")\nasync def api_scroll():\n page = int(request.args.get(\"page\", 2))\n items = [f\"Item {i}\" for i in range(...)]\n # Include next sentinel if more pages\n return sx_response(items_sx + sentinel_sx)"
|
||||
:handler-code (handler-source "ex-scroll")
|
||||
:comp-placeholder-id "scroll-comp"
|
||||
:wire-placeholder-id "scroll-wire"))
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
:demo-description "Click start to begin a simulated job."
|
||||
:demo (~progress-bar-demo)
|
||||
:sx-code ";; Start the job\n(button\n :sx-post \"/geography/hypermedia/examples/api/progress/start\"\n :sx-target \"#progress-target\"\n :sx-swap \"innerHTML\")\n\n;; Each response re-polls via sx-trigger=\"load\"\n(div :sx-get \"/api/progress/status?job=ID\"\n :sx-trigger \"load delay:500ms\"\n :sx-target \"#progress-target\"\n :sx-swap \"innerHTML\")"
|
||||
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/progress/start\")\nasync def api_progress_start():\n job_id = str(uuid4())[:8]\n _jobs[job_id] = 0\n return sx_response(\n f'(~progress-status :percent 0 :job-id \"{job_id}\")')"
|
||||
:handler-code (str (handler-source "ex-progress-start") "\n\n" (handler-source "ex-progress-status"))
|
||||
:comp-placeholder-id "progress-comp"
|
||||
:wire-placeholder-id "progress-wire"))
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
:demo-description "Type to search through 20 programming languages."
|
||||
:demo (~active-search-demo)
|
||||
:sx-code "(input :type \"text\" :name \"q\"\n :sx-get \"/geography/hypermedia/examples/api/search\"\n :sx-trigger \"keyup delay:300ms changed\"\n :sx-target \"#search-results\"\n :sx-swap \"innerHTML\"\n :placeholder \"Search...\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/search\")\nasync def api_search():\n q = request.args.get(\"q\", \"\").lower()\n results = [l for l in LANGUAGES if q in l.lower()]\n return sx_response(\n f'(~search-results :items (...) :query \"{q}\")')"
|
||||
:handler-code (handler-source "ex-search")
|
||||
:comp-placeholder-id "search-comp"
|
||||
:wire-placeholder-id "search-wire"))
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
:demo-description "Enter an email and click away (blur) to validate."
|
||||
:demo (~inline-validation-demo)
|
||||
:sx-code "(input :type \"text\" :name \"email\"\n :sx-get \"/geography/hypermedia/examples/api/validate\"\n :sx-trigger \"blur\"\n :sx-target \"#email-feedback\"\n :sx-swap \"innerHTML\"\n :placeholder \"user@example.com\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/validate\")\nasync def api_validate():\n email = request.args.get(\"email\", \"\")\n if \"@\" not in email:\n return sx_response('(~validation-error ...)')\n return sx_response('(~validation-ok ...)')"
|
||||
:handler-code (handler-source "ex-validate")
|
||||
:comp-placeholder-id "validate-comp"
|
||||
:wire-placeholder-id "validate-wire"))
|
||||
|
||||
@@ -140,7 +140,7 @@
|
||||
:demo-description "Select a category to populate the item dropdown."
|
||||
:demo (~value-select-demo)
|
||||
:sx-code "(select :name \"category\"\n :sx-get \"/geography/hypermedia/examples/api/values\"\n :sx-trigger \"change\"\n :sx-target \"#value-items\"\n :sx-swap \"innerHTML\"\n (option \"Languages\")\n (option \"Frameworks\")\n (option \"Databases\"))"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/values\")\nasync def api_values():\n cat = request.args.get(\"category\", \"\")\n items = VALUE_SELECT_DATA.get(cat, [])\n return sx_response(\n f'(~value-options :items (list ...))')"
|
||||
:handler-code (handler-source "ex-values")
|
||||
:comp-placeholder-id "values-comp"
|
||||
:wire-placeholder-id "values-wire"))
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
:demo-description "Submit a message — the input resets after each send."
|
||||
:demo (~reset-on-submit-demo)
|
||||
:sx-code "(form :id \"reset-form\"\n :sx-post \"/geography/hypermedia/examples/api/reset-submit\"\n :sx-target \"#reset-result\"\n :sx-swap \"innerHTML\"\n :sx-on:afterSwap \"this.reset()\"\n (input :type \"text\" :name \"message\")\n (button :type \"submit\" \"Send\"))"
|
||||
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/reset-submit\")\nasync def api_reset_submit():\n form = await request.form\n msg = form.get(\"message\", \"\")\n return sx_response(\n f'(~reset-message :message \"{msg}\" :time \"...\")')"
|
||||
:handler-code (handler-source "ex-reset-submit")
|
||||
:comp-placeholder-id "reset-comp"
|
||||
:wire-placeholder-id "reset-wire"))
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
(list "3" "Widget C" "12.00" "305")
|
||||
(list "4" "Widget D" "45.00" "67")))
|
||||
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/editrow/1\"\n :sx-target \"#erow-1\"\n :sx-swap \"outerHTML\"\n \"edit\")\n\n;; Save sends form data via POST\n(button\n :sx-post \"/geography/hypermedia/examples/api/editrow/1\"\n :sx-target \"#erow-1\"\n :sx-swap \"outerHTML\"\n :sx-include \"#erow-1\"\n \"save\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/editrow/<id>\")\nasync def api_editrow_form(id):\n row = EDIT_ROW_DATA[id]\n return sx_response(\n f'(~edit-row-form :id ... :name ...)')\n\n@bp.post(\"/geography/hypermedia/examples/api/editrow/<id>\")\nasync def api_editrow_save(id):\n form = await request.form\n return sx_response(\n f'(~edit-row-view :id ... :name ...)')"
|
||||
:handler-code (str (handler-source "ex-editrow-form") "\n\n" (handler-source "ex-editrow-save"))
|
||||
:comp-placeholder-id "editrow-comp"
|
||||
:wire-placeholder-id "editrow-wire"))
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
(list "4" "Dan Okafor" "dan@example.com" "inactive")
|
||||
(list "5" "Eve Larsson" "eve@example.com" "active")))
|
||||
:sx-code "(button\n :sx-post \"/geography/hypermedia/examples/api/bulk?action=activate\"\n :sx-target \"#bulk-table\"\n :sx-swap \"innerHTML\"\n :sx-include \"#bulk-form\"\n \"Activate\")"
|
||||
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/bulk\")\nasync def api_bulk():\n action = request.args.get(\"action\")\n form = await request.form\n ids = form.getlist(\"ids\")\n # Update matching users\n return sx_response(updated_rows)"
|
||||
:handler-code (handler-source "ex-bulk")
|
||||
:comp-placeholder-id "bulk-comp"
|
||||
:wire-placeholder-id "bulk-wire"))
|
||||
|
||||
@@ -193,7 +193,7 @@
|
||||
:demo-description "Try each button to see different swap behaviours."
|
||||
:demo (~swap-positions-demo)
|
||||
:sx-code ";; Append to end\n(button :sx-post \"/api/swap-log?mode=beforeend\"\n :sx-target \"#swap-log\" :sx-swap \"beforeend\"\n \"Add to End\")\n\n;; Prepend to start\n(button :sx-post \"/api/swap-log?mode=afterbegin\"\n :sx-target \"#swap-log\" :sx-swap \"afterbegin\"\n \"Add to Start\")\n\n;; No swap — OOB counter update only\n(button :sx-post \"/api/swap-log?mode=none\"\n :sx-target \"#swap-log\" :sx-swap \"none\"\n \"Silent Ping\")"
|
||||
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/swap-log\")\nasync def api_swap_log():\n mode = request.args.get(\"mode\")\n # OOB counter updates on every request\n oob = f'(span :id \"swap-counter\" :sx-swap-oob \"innerHTML\" \"Count: {n}\")'\n return sx_response(entry + oob)"
|
||||
:handler-code (handler-source "ex-swap-log")
|
||||
:wire-placeholder-id "swap-wire"))
|
||||
|
||||
(defcomp ~example-select-filter ()
|
||||
@@ -203,7 +203,7 @@
|
||||
:demo-description "Different buttons select different parts of the same server response."
|
||||
:demo (~select-filter-demo)
|
||||
:sx-code ";; Pick just the stats section from the response\n(button\n :sx-get \"/geography/hypermedia/examples/api/dashboard\"\n :sx-target \"#filter-target\"\n :sx-swap \"innerHTML\"\n :sx-select \"#dash-stats\"\n \"Stats Only\")\n\n;; No sx-select — get the full response\n(button\n :sx-get \"/geography/hypermedia/examples/api/dashboard\"\n :sx-target \"#filter-target\"\n :sx-swap \"innerHTML\"\n \"Full Dashboard\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/dashboard\")\nasync def api_dashboard():\n # Returns header + stats + footer\n # Client uses sx-select to pick sections\n return sx_response(\n '(<> (div :id \"dash-header\" ...) '\n ' (div :id \"dash-stats\" ...) '\n ' (div :id \"dash-footer\" ...))')"
|
||||
:handler-code (handler-source "ex-dashboard")
|
||||
:wire-placeholder-id "filter-wire"))
|
||||
|
||||
(defcomp ~example-tabs ()
|
||||
@@ -213,7 +213,7 @@
|
||||
:demo-description "Click tabs to switch content. Watch the browser URL change."
|
||||
:demo (~tabs-demo)
|
||||
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/tabs/tab1\"\n :sx-target \"#tab-content\"\n :sx-swap \"innerHTML\"\n :sx-push-url \"/geography/hypermedia/examples/tabs?tab=tab1\"\n \"Overview\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/tabs/<tab>\")\nasync def api_tabs(tab: str):\n content = TAB_CONTENT[tab]\n return sx_response(content)"
|
||||
:handler-code (handler-source "ex-tabs")
|
||||
:wire-placeholder-id "tabs-wire"))
|
||||
|
||||
(defcomp ~example-animations ()
|
||||
@@ -223,7 +223,7 @@
|
||||
:demo-description "Click to swap in content with a fade-in animation."
|
||||
:demo (~animations-demo)
|
||||
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/animate\"\n :sx-target \"#anim-target\"\n :sx-swap \"innerHTML\"\n \"Load with animation\")\n\n;; Component uses CSS animation class\n(defcomp ~anim-result (&key color time)\n (div :class \"sx-fade-in ...\"\n (style \".sx-fade-in { animation: sxFadeIn 0.5s }\")\n (p \"Faded in!\")))"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/animate\")\nasync def api_animate():\n colors = [\"bg-violet-100\", \"bg-emerald-100\", ...]\n color = random.choice(colors)\n return sx_response(\n f'(~anim-result :color \"{color}\" :time \"{now}\")')"
|
||||
:handler-code (handler-source "ex-animate")
|
||||
:comp-placeholder-id "anim-comp"
|
||||
:wire-placeholder-id "anim-wire"))
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
:demo-description "Click to open a modal dialog."
|
||||
:demo (~dialogs-demo)
|
||||
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/dialog\"\n :sx-target \"#dialog-container\"\n :sx-swap \"innerHTML\"\n \"Open Dialog\")\n\n;; Dialog closes by swapping empty content\n(button\n :sx-get \"/geography/hypermedia/examples/api/dialog/close\"\n :sx-target \"#dialog-container\"\n :sx-swap \"innerHTML\"\n \"Close\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/dialog\")\nasync def api_dialog():\n return sx_response(\n '(~dialog-modal :title \"Confirm\"'\n ' :message \"Are you sure?\")')\n\n@bp.get(\"/geography/hypermedia/examples/api/dialog/close\")\nasync def api_dialog_close():\n return sx_response(\"\")"
|
||||
:handler-code (str (handler-source "ex-dialog") "\n\n" (handler-source "ex-dialog-close"))
|
||||
:comp-placeholder-id "dialog-comp"
|
||||
:wire-placeholder-id "dialog-wire"))
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
:demo-description "Press s, n, or h on your keyboard."
|
||||
:demo (~keyboard-shortcuts-demo)
|
||||
:sx-code "(div :id \"kbd-target\"\n :sx-get \"/geography/hypermedia/examples/api/keyboard?key=s\"\n :sx-trigger \"keyup[key=='s'&&!event.target.matches('input,textarea')] from:body\"\n :sx-swap \"innerHTML\"\n \"Press a shortcut key...\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/keyboard\")\nasync def api_keyboard():\n key = request.args.get(\"key\", \"\")\n actions = {\"s\": \"Search\", \"n\": \"New item\", \"h\": \"Help\"}\n return sx_response(\n f'(~kbd-result :key \"{key}\" :action \"{actions[key]}\")')"
|
||||
:handler-code (handler-source "ex-keyboard")
|
||||
:comp-placeholder-id "kbd-comp"
|
||||
:wire-placeholder-id "kbd-wire"))
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
:demo-description "Click Edit All to replace the full profile via PUT."
|
||||
:demo (~put-patch-demo :name "Ada Lovelace" :email "ada@example.com" :role "Engineer")
|
||||
:sx-code ";; Replace entire resource\n(form :sx-put \"/geography/hypermedia/examples/api/putpatch\"\n :sx-target \"#pp-target\" :sx-swap \"innerHTML\"\n (input :name \"name\") (input :name \"email\")\n (button \"Save All (PUT)\"))"
|
||||
:handler-code "@bp.put(\"/geography/hypermedia/examples/api/putpatch\")\nasync def api_put():\n form = await request.form\n # Full replacement\n return sx_response('(~pp-view ...)')"
|
||||
:handler-code (str (handler-source "ex-pp-edit-all") "\n\n" (handler-source "ex-pp-put"))
|
||||
:comp-placeholder-id "pp-comp"
|
||||
:wire-placeholder-id "pp-wire"))
|
||||
|
||||
@@ -267,7 +267,7 @@
|
||||
:demo-description "Submit the form and see the JSON body the server received."
|
||||
:demo (~json-encoding-demo)
|
||||
:sx-code "(form\n :sx-post \"/geography/hypermedia/examples/api/json-echo\"\n :sx-target \"#json-result\"\n :sx-swap \"innerHTML\"\n :sx-encoding \"json\"\n (input :name \"name\" :value \"Ada\")\n (input :type \"number\" :name \"age\" :value \"36\")\n (button \"Submit as JSON\"))"
|
||||
:handler-code "@bp.post(\"/geography/hypermedia/examples/api/json-echo\")\nasync def api_json_echo():\n data = await request.get_json()\n body = json.dumps(data, indent=2)\n ct = request.content_type\n return sx_response(\n f'(~json-result :body \"{body}\" :content-type \"{ct}\")')"
|
||||
:handler-code (handler-source "ex-json-echo")
|
||||
:comp-placeholder-id "json-comp"
|
||||
:wire-placeholder-id "json-wire"))
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
:demo-description "Click each button to see what the server receives."
|
||||
:demo (~vals-headers-demo)
|
||||
:sx-code ";; Send extra values with the request\n(button\n :sx-get \"/geography/hypermedia/examples/api/echo-vals\"\n :sx-vals \"{\\\"source\\\": \\\"button\\\"}\"\n \"Send with vals\")\n\n;; Send custom headers\n(button\n :sx-get \"/geography/hypermedia/examples/api/echo-headers\"\n :sx-headers {:X-Custom-Token \"abc123\"}\n \"Send with headers\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/echo-vals\")\nasync def api_echo_vals():\n vals = dict(request.args)\n return sx_response(\n f'(~echo-result :label \"values\" :items (...))')\n\n@bp.get(\"/geography/hypermedia/examples/api/echo-headers\")\nasync def api_echo_headers():\n custom = {k: v for k, v in request.headers\n if k.startswith(\"X-\")}\n return sx_response(\n f'(~echo-result :label \"headers\" :items (...))')"
|
||||
:handler-code (str (handler-source "ex-echo-vals") "\n\n" (handler-source "ex-echo-headers"))
|
||||
:comp-placeholder-id "vals-comp"
|
||||
:wire-placeholder-id "vals-wire"))
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
:demo-description "Click the button — it shows a spinner during the 2-second request."
|
||||
:demo (~loading-states-demo)
|
||||
:sx-code ";; .sx-request class added during request\n(style \".sx-loading-btn.sx-request {\n opacity: 0.7; pointer-events: none; }\n.sx-loading-btn.sx-request .sx-spinner {\n display: inline-block; }\n.sx-loading-btn .sx-spinner {\n display: none; }\")\n\n(button :class \"sx-loading-btn\"\n :sx-get \"/geography/hypermedia/examples/api/slow\"\n :sx-target \"#loading-result\"\n (span :class \"sx-spinner animate-spin\" \"...\")\n \"Load slow endpoint\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/slow\")\nasync def api_slow():\n await asyncio.sleep(2)\n return sx_response(\n f'(~loading-result :time \"{now}\")')"
|
||||
:handler-code (handler-source "ex-slow")
|
||||
:comp-placeholder-id "loading-comp"
|
||||
:wire-placeholder-id "loading-wire"))
|
||||
|
||||
@@ -300,7 +300,7 @@
|
||||
:demo-description "Type quickly — only the latest result appears despite random 0.5-2s server delays."
|
||||
:demo (~sync-replace-demo)
|
||||
:sx-code "(input :type \"text\" :name \"q\"\n :sx-get \"/geography/hypermedia/examples/api/slow-search\"\n :sx-trigger \"keyup delay:200ms changed\"\n :sx-target \"#sync-result\"\n :sx-swap \"innerHTML\"\n :sx-sync \"replace\"\n \"Type to search...\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/slow-search\")\nasync def api_slow_search():\n delay = random.uniform(0.5, 2.0)\n await asyncio.sleep(delay)\n q = request.args.get(\"q\", \"\")\n return sx_response(\n f'(~sync-result :query \"{q}\" :delay \"{delay_ms}\")')"
|
||||
:handler-code (handler-source "ex-slow-search")
|
||||
:comp-placeholder-id "sync-comp"
|
||||
:wire-placeholder-id "sync-wire"))
|
||||
|
||||
@@ -311,6 +311,6 @@
|
||||
:demo-description "Click the button — watch it retry automatically after failures."
|
||||
:demo (~retry-demo)
|
||||
:sx-code "(button\n :sx-get \"/geography/hypermedia/examples/api/flaky\"\n :sx-target \"#retry-result\"\n :sx-swap \"innerHTML\"\n :sx-retry \"exponential:1000:8000\"\n \"Call flaky endpoint\")"
|
||||
:handler-code "@bp.get(\"/geography/hypermedia/examples/api/flaky\")\nasync def api_flaky():\n _flaky[\"n\"] += 1\n if _flaky[\"n\"] % 3 != 0:\n return Response(\"\", status=503)\n return sx_response(\n f'(~retry-result :attempt {n} ...)')"
|
||||
:handler-code (handler-source "ex-flaky")
|
||||
:comp-placeholder-id "retry-comp"
|
||||
:wire-placeholder-id "retry-wire"))
|
||||
|
||||
@@ -24,19 +24,9 @@
|
||||
(list "admin@example.com" "test@example.com" "user@example.com"))
|
||||
|
||||
(define tab-content
|
||||
{"tab1" (div (p :class "text-stone-700" "Welcome to the Overview tab.")
|
||||
(p :class "text-stone-500 text-sm mt-2"
|
||||
"This is the default tab content loaded via sx-get."))
|
||||
"tab2" (div (p :class "text-stone-700" "Here are the details.")
|
||||
(ul :class "mt-2 space-y-1 text-sm text-stone-600"
|
||||
(li "Version: 1.0.0")
|
||||
(li "Build: 2024-01-15")
|
||||
(li "Engine: sx")))
|
||||
"tab3" (div (p :class "text-stone-700" "Recent history:")
|
||||
(ol :class "mt-2 space-y-1 text-sm text-stone-600 list-decimal list-inside"
|
||||
(li "Initial release")
|
||||
(li "Added component caching")
|
||||
(li "Wire format v2")))})
|
||||
{"tab1" "Welcome to the Overview tab. This is the default tab content loaded via sx-get."
|
||||
"tab2" "Here are the details. Version: 1.0.0, Build: 2024-01-15, Engine: sx"
|
||||
"tab3" "Recent history: Initial release, Added component caching, Wire format v2"})
|
||||
|
||||
(define kbd-actions
|
||||
{"s" "Search panel activated"
|
||||
|
||||
@@ -1,367 +0,0 @@
|
||||
;; SX example API handlers — defhandler definitions
|
||||
;;
|
||||
;; These serve the live demos on the Examples docs pages.
|
||||
;; Each handler's source is displayed in the "Server handler" code block
|
||||
;; on its corresponding example page (self-referencing via handler-source).
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Click to Load
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler click (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(~click-result :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Form Submission
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler form (&key)
|
||||
(let ((name (form-data "name")))
|
||||
(~form-result :name name)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Polling
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler poll (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S"))
|
||||
(count (inc-counter "poll" :max 10)))
|
||||
(~poll-result :time now :count count)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Delete Row
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler delete (&key item-id)
|
||||
;; Empty response — outerHTML swap removes the row
|
||||
"")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Inline Edit
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler edit-form (&key)
|
||||
(let ((value (request-arg "value")))
|
||||
(~inline-edit-form :value value)))
|
||||
|
||||
(defhandler edit-save (&key)
|
||||
(let ((value (form-data "value")))
|
||||
(~inline-view :value value)))
|
||||
|
||||
(defhandler edit-cancel (&key)
|
||||
(let ((value (request-arg "value")))
|
||||
(~inline-view :value value)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Out-of-Band Swaps
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler oob (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(<>
|
||||
(p :class "text-emerald-600 font-medium" "Box A updated!")
|
||||
(p :class "text-sm text-stone-500" "at " now)
|
||||
(div :id "oob-box-b" :sx-swap-oob "innerHTML"
|
||||
(p :class "text-violet-600 font-medium" "Box B updated via OOB!")
|
||||
(p :class "text-sm text-stone-500" "at " now)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Lazy Loading
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler lazy (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(~lazy-result :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Infinite Scroll
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler scroll (&key)
|
||||
(let ((page (or (parse-int (request-arg "page")) 2))
|
||||
(start (+ (* (- page 1) 5) 1))
|
||||
(next (+ page 1)))
|
||||
(<>
|
||||
(map (fn (i)
|
||||
(div :class "px-4 py-3 border-b border-stone-100 text-sm text-stone-700"
|
||||
"Item " i " — loaded from page " page))
|
||||
(range start (+ start 5)))
|
||||
(if (<= next 6)
|
||||
(div :id "scroll-sentinel"
|
||||
:sx-get (str "/geography/hypermedia/examples/api/scroll?page=" next)
|
||||
:sx-trigger "intersect once"
|
||||
:sx-target "#scroll-items"
|
||||
:sx-swap "beforeend"
|
||||
:class "p-3 text-center text-stone-400 text-sm"
|
||||
"Loading more...")
|
||||
(div :class "p-3 text-center text-stone-500 text-sm font-medium"
|
||||
"All items loaded.")))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Progress Bar
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler progress-start (&key)
|
||||
(let ((job-id (new-job)))
|
||||
(~progress-status :percent 0 :job-id job-id)))
|
||||
|
||||
(defhandler progress-status (&key)
|
||||
(let ((job-id (request-arg "job"))
|
||||
(percent (advance-job job-id)))
|
||||
(~progress-status :percent percent :job-id job-id)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Active Search
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler search (&key)
|
||||
(let ((q (request-arg "q"))
|
||||
(results (filter-list LANGUAGES q)))
|
||||
(~search-results :items results :query q)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Inline Validation
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler validate (&key)
|
||||
(let ((email (request-arg "email")))
|
||||
(cond
|
||||
((not email)
|
||||
(~validation-error :message "Email is required"))
|
||||
((not (contains? email "@"))
|
||||
(~validation-error :message "Invalid email format"))
|
||||
((contains? TAKEN_EMAILS (lower email))
|
||||
(~validation-error
|
||||
:message (str email " is already taken")))
|
||||
(t (~validation-ok :email email)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Value Select
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler values (&key)
|
||||
(let ((cat (request-arg "category"))
|
||||
(items (get VALUE_SELECT_DATA cat)))
|
||||
(if (empty? items)
|
||||
(option :value "" "No items")
|
||||
(map (fn (i) (option :value i i)) items))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Reset on Submit
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler reset-submit (&key)
|
||||
(let ((msg (or (form-data "message") "(empty)"))
|
||||
(now (format-time (now) "%H:%M:%S")))
|
||||
(~reset-message :message msg :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Edit Row
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler editrow-form (&key row-id)
|
||||
(let ((row (get ROWS row-id)))
|
||||
(~edit-row-form :id row-id
|
||||
:name (get row "name")
|
||||
:price (get row "price")
|
||||
:stock (get row "stock"))))
|
||||
|
||||
(defhandler editrow-save (&key row-id)
|
||||
(let ((name (form-data "name"))
|
||||
(price (form-data "price"))
|
||||
(stock (form-data "stock")))
|
||||
(~edit-row-view :id row-id
|
||||
:name name :price price :stock stock)))
|
||||
|
||||
(defhandler editrow-cancel (&key row-id)
|
||||
(let ((row (get ROWS row-id)))
|
||||
(~edit-row-view :id row-id
|
||||
:name (get row "name")
|
||||
:price (get row "price")
|
||||
:stock (get row "stock"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Bulk Update
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler bulk (&key)
|
||||
(let ((action (request-arg "action"))
|
||||
(ids (form-list "ids"))
|
||||
(status (if (= action "activate")
|
||||
"active" "inactive")))
|
||||
(update-users ids :status status)
|
||||
(map (fn (u)
|
||||
(~bulk-row
|
||||
:id (get u "id")
|
||||
:name (get u "name")
|
||||
:email (get u "email")
|
||||
:status (get u "status")))
|
||||
USERS)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Swap Positions
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler swap-log (&key)
|
||||
(let ((mode (request-arg "mode"))
|
||||
(n (inc-counter "swap"))
|
||||
(now (format-time (now) "%H:%M:%S")))
|
||||
(<>
|
||||
(div :class "px-3 py-2 text-sm text-stone-700"
|
||||
"[" now "] " mode " (#" n ")")
|
||||
(span :id "swap-counter"
|
||||
:sx-swap-oob "innerHTML"
|
||||
:class "self-center text-sm text-stone-500"
|
||||
"Count: " n))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Select Filter (Dashboard)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler dashboard (&key)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(<>
|
||||
(div :id "dash-header" :class "p-3 bg-violet-50 rounded mb-3"
|
||||
(h4 :class "font-semibold text-violet-800" "Dashboard Header")
|
||||
(p :class "text-sm text-violet-600" "Generated at " now))
|
||||
(div :id "dash-stats" :class "grid grid-cols-3 gap-3 mb-3"
|
||||
(div :class "p-3 bg-emerald-50 rounded text-center"
|
||||
(p :class "text-2xl font-bold text-emerald-700" "142")
|
||||
(p :class "text-xs text-emerald-600" "Users"))
|
||||
(div :class "p-3 bg-blue-50 rounded text-center"
|
||||
(p :class "text-2xl font-bold text-blue-700" "89")
|
||||
(p :class "text-xs text-blue-600" "Orders"))
|
||||
(div :class "p-3 bg-amber-50 rounded text-center"
|
||||
(p :class "text-2xl font-bold text-amber-700" "$4.2k")
|
||||
(p :class "text-xs text-amber-600" "Revenue")))
|
||||
(div :id "dash-footer" :class "p-3 bg-stone-100 rounded"
|
||||
(p :class "text-sm text-stone-500" "Last updated: " now)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Tabs
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler tabs (&key tab)
|
||||
(let ((content (get TAB_CONTENT tab)))
|
||||
(<> content
|
||||
(div :id "tab-buttons"
|
||||
:sx-swap-oob "innerHTML"
|
||||
:class "flex border-b border-stone-200"
|
||||
(map (fn (t)
|
||||
(~tab-btn
|
||||
:tab (first t)
|
||||
:label (last t)
|
||||
:active (if (= (first t) tab) "true" "false")))
|
||||
TAB_LIST)))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Animations
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler animate (&key)
|
||||
(let ((color (random-choice
|
||||
"bg-violet-100" "bg-emerald-100"
|
||||
"bg-blue-100" "bg-amber-100"))
|
||||
(now (format-time (now) "%H:%M:%S")))
|
||||
(~anim-result :color color :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Dialogs
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler dialog (&key)
|
||||
(~dialog-modal
|
||||
:title "Confirm Action"
|
||||
:message "Are you sure you want to proceed?"))
|
||||
|
||||
(defhandler dialog-close (&key)
|
||||
"")
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Keyboard Shortcuts
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler keyboard (&key)
|
||||
(let ((key (request-arg "key"))
|
||||
(actions {:s "Search panel activated"
|
||||
:n "New item created"
|
||||
:h "Help panel opened"})
|
||||
(action (get actions key)))
|
||||
(~kbd-result :key key :action action)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; PUT / PATCH
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler pp-edit-all (&key)
|
||||
(let ((p (get-profile)))
|
||||
(~pp-form-full
|
||||
:name (get p "name")
|
||||
:email (get p "email")
|
||||
:role (get p "role"))))
|
||||
|
||||
(defhandler put-profile (&key)
|
||||
(let ((name (form-data "name"))
|
||||
(email (form-data "email"))
|
||||
(role (form-data "role")))
|
||||
(~pp-view :name name :email email :role role)))
|
||||
|
||||
(defhandler pp-cancel (&key)
|
||||
(let ((p (get-profile)))
|
||||
(~pp-view
|
||||
:name (get p "name")
|
||||
:email (get p "email")
|
||||
:role (get p "role"))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; JSON Encoding
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler json-echo (&key)
|
||||
(let ((data (request-json))
|
||||
(body (json-pretty data))
|
||||
(ct (request-header "content-type")))
|
||||
(~json-result :body body :content-type ct)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Vals & Headers
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler echo-vals (&key)
|
||||
(let ((vals (request-args)))
|
||||
(~echo-result :label "values" :items vals)))
|
||||
|
||||
(defhandler echo-headers (&key)
|
||||
(let ((headers (request-headers :prefix "X-")))
|
||||
(~echo-result :label "headers" :items headers)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Loading States
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler slow (&key)
|
||||
(sleep 2000)
|
||||
(let ((now (format-time (now) "%H:%M:%S")))
|
||||
(~loading-result :time now)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Request Abort (sync replace)
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler slow-search (&key)
|
||||
(let ((delay (random-int 500 2000)))
|
||||
(sleep delay)
|
||||
(let ((q (request-arg "q")))
|
||||
(~sync-result :query q :delay delay))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Retry
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(defhandler flaky (&key)
|
||||
(let ((n (inc-counter "flaky")))
|
||||
(if (!= (mod n 3) 0)
|
||||
(error 503)
|
||||
(~retry-result :attempt n
|
||||
:message "Success! The endpoint finally responded."))))
|
||||
@@ -35,6 +35,7 @@ def _register_sx_helpers() -> None:
|
||||
"prove-data": _prove_data,
|
||||
"page-helpers-demo-data": _page_helpers_demo_data,
|
||||
"spec-explorer-data": _spec_explorer_data,
|
||||
"handler-source": _handler_source,
|
||||
})
|
||||
|
||||
|
||||
@@ -68,6 +69,35 @@ def _component_source(name: str) -> str:
|
||||
})
|
||||
|
||||
|
||||
def _handler_source(name: str) -> str:
|
||||
"""Return the pretty-printed defhandler source for a named handler."""
|
||||
from shared.sx.handlers import get_handler
|
||||
from shared.sx.parser import serialize
|
||||
|
||||
hdef = get_handler("sx", name)
|
||||
if not hdef:
|
||||
return f";;; Handler not found: {name}"
|
||||
|
||||
parts = [f"(defhandler {hdef.name}"]
|
||||
if hdef.path:
|
||||
parts.append(f' :path "{hdef.path}"')
|
||||
if hdef.method != "get":
|
||||
parts.append(f" :method :{hdef.method}")
|
||||
if not hdef.csrf:
|
||||
parts.append(" :csrf false")
|
||||
if hdef.returns != "element":
|
||||
parts.append(f' :returns "{hdef.returns}"')
|
||||
param_strs = ["&key"] + list(hdef.params) if hdef.params else []
|
||||
parts.append(f" ({' '.join(param_strs)})" if param_strs else " ()")
|
||||
body_sx = serialize(hdef.body, pretty=True)
|
||||
# Indent body by 2 spaces
|
||||
body_lines = body_sx.split("\n")
|
||||
parts.append(" " + body_lines[0])
|
||||
for line in body_lines[1:]:
|
||||
parts.append(" " + line)
|
||||
return "\n".join(parts) + ")"
|
||||
|
||||
|
||||
def _primitives_data() -> dict:
|
||||
"""Return the PRIMITIVES dict for the primitives docs page."""
|
||||
from content.pages import PRIMITIVES
|
||||
|
||||
Reference in New Issue
Block a user