Live wire response + component display with OOB swaps on all examples
- All 6 examples show Component and Wire response as placeholders that fill with actual content when the demo is triggered (via OOB swaps) - Wire response shows full wire content including component definitions (when not cached) and CSS style block - Component display only includes defs the client doesn't already have, matching real sx_response() behaviour - Add "Clear component cache" button to reset localStorage + in-memory component env so next interaction shows component download - Rebuild tw.css with Tailwind v3.4.19 including sx content paths - Optimize sx_response() CSS scanning to only scan sent comp_defs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,8 @@ module.exports = {
|
|||||||
safelist: [
|
safelist: [
|
||||||
// ~menu-row-sx builds bg-{colour}-{shade} dynamically via (str ...)
|
// ~menu-row-sx builds bg-{colour}-{shade} dynamically via (str ...)
|
||||||
// Levels 1–4 produce shades 400–100 (level 5+ yields 0 or negative = no match)
|
// Levels 1–4 produce shades 400–100 (level 5+ yields 0 or negative = no match)
|
||||||
{ pattern: /^bg-sky-(100|200|300|400)$/ },
|
{ pattern: /^bg-sky-(100|200|300|400|500)$/ },
|
||||||
|
{ pattern: /^bg-violet-(100|200|300|400|500)$/ },
|
||||||
],
|
],
|
||||||
content: [
|
content: [
|
||||||
'/root/rose-ash/shared/sx/templates/**/*.sx',
|
'/root/rose-ash/shared/sx/templates/**/*.sx',
|
||||||
@@ -30,6 +31,9 @@ module.exports = {
|
|||||||
'/root/rose-ash/federation/sx/sx_components.py',
|
'/root/rose-ash/federation/sx/sx_components.py',
|
||||||
'/root/rose-ash/account/sx/sx_components.py',
|
'/root/rose-ash/account/sx/sx_components.py',
|
||||||
'/root/rose-ash/orders/sx/sx_components.py',
|
'/root/rose-ash/orders/sx/sx_components.py',
|
||||||
|
'/root/rose-ash/sx/sxc/**/*.sx',
|
||||||
|
'/root/rose-ash/sx/sxc/sx_components.py',
|
||||||
|
'/root/rose-ash/sx/content/highlight.py',
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -402,8 +402,6 @@ def sx_response(source_or_component: str, status: int = 200,
|
|||||||
|
|
||||||
# On-demand CSS: scan source for classes, send only new rules
|
# On-demand CSS: scan source for classes, send only new rules
|
||||||
from .css_registry import scan_classes_from_sx, lookup_rules, registry_loaded, lookup_css_hash, store_css_hash
|
from .css_registry import scan_classes_from_sx, lookup_rules, registry_loaded, lookup_css_hash, store_css_hash
|
||||||
from .jinja_bridge import _COMPONENT_ENV
|
|
||||||
from .types import Component as _Component
|
|
||||||
new_classes: set[str] = set()
|
new_classes: set[str] = set()
|
||||||
cumulative_classes: set[str] = set()
|
cumulative_classes: set[str] = set()
|
||||||
if registry_loaded():
|
if registry_loaded():
|
||||||
@@ -411,10 +409,8 @@ def sx_response(source_or_component: str, status: int = 200,
|
|||||||
# Include pre-computed helper classes (menu bars, admin nav, etc.)
|
# Include pre-computed helper classes (menu bars, admin nav, etc.)
|
||||||
new_classes.update(HELPER_CSS_CLASSES)
|
new_classes.update(HELPER_CSS_CLASSES)
|
||||||
if comp_defs:
|
if comp_defs:
|
||||||
# Use pre-computed classes for components being sent
|
# Scan only the component definitions actually being sent
|
||||||
for key, val in _COMPONENT_ENV.items():
|
new_classes.update(scan_classes_from_sx(comp_defs))
|
||||||
if isinstance(val, _Component) and val.css_classes:
|
|
||||||
new_classes.update(val.css_classes)
|
|
||||||
|
|
||||||
# Resolve known classes from SX-Css header (hash or full list)
|
# Resolve known classes from SX-Css header (hash or full list)
|
||||||
known_classes: set[str] = set()
|
known_classes: set[str] = set()
|
||||||
|
|||||||
@@ -132,58 +132,103 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
@bp.get("/examples/api/click")
|
@bp.get("/examples/api/click")
|
||||||
async def api_click():
|
async def api_click():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
return sx_response('(~click-result)')
|
from sxc.sx_components 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})')
|
||||||
|
|
||||||
@bp.post("/examples/api/form")
|
@bp.post("/examples/api/form")
|
||||||
async def api_form():
|
async def api_form():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||||
form = await request.form
|
form = await request.form
|
||||||
name = form.get("name", "")
|
name = form.get("name", "")
|
||||||
escaped = name.replace('"', '\\"')
|
escaped = name.replace('"', '\\"')
|
||||||
return sx_response(f'(~form-result :name "{escaped}")')
|
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}
|
_poll_count = {"n": 0}
|
||||||
|
|
||||||
@bp.get("/examples/api/poll")
|
@bp.get("/examples/api/poll")
|
||||||
async def api_poll():
|
async def api_poll():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||||
_poll_count["n"] += 1
|
_poll_count["n"] += 1
|
||||||
now = datetime.now().strftime("%H:%M:%S")
|
now = datetime.now().strftime("%H:%M:%S")
|
||||||
count = min(_poll_count["n"], 10)
|
count = min(_poll_count["n"], 10)
|
||||||
return sx_response(f'(~poll-result :time "{now}" :count {count})')
|
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})')
|
||||||
|
|
||||||
@bp.delete("/examples/api/delete/<item_id>")
|
@bp.delete("/examples/api/delete/<item_id>")
|
||||||
async def api_delete(item_id: str):
|
async def api_delete(item_id: str):
|
||||||
# Return empty response — the row's outerHTML swap removes it
|
from shared.sx.helpers import sx_response
|
||||||
return Response("", status=200, content_type="text/sx")
|
from sxc.sx_components 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("/examples/api/edit")
|
@bp.get("/examples/api/edit")
|
||||||
async def api_edit_form():
|
async def api_edit_form():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||||
value = request.args.get("value", "")
|
value = request.args.get("value", "")
|
||||||
escaped = value.replace('"', '\\"')
|
escaped = value.replace('"', '\\"')
|
||||||
return sx_response(f'(~inline-edit-form :value "{escaped}")')
|
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})')
|
||||||
|
|
||||||
@bp.post("/examples/api/edit")
|
@bp.post("/examples/api/edit")
|
||||||
async def api_edit_save():
|
async def api_edit_save():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||||
form = await request.form
|
form = await request.form
|
||||||
value = form.get("value", "")
|
value = form.get("value", "")
|
||||||
escaped = value.replace('"', '\\"')
|
escaped = value.replace('"', '\\"')
|
||||||
return sx_response(f'(~inline-view :value "{escaped}")')
|
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("/examples/api/edit/cancel")
|
@bp.get("/examples/api/edit/cancel")
|
||||||
async def api_edit_cancel():
|
async def api_edit_cancel():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||||
value = request.args.get("value", "")
|
value = request.args.get("value", "")
|
||||||
escaped = value.replace('"', '\\"')
|
escaped = value.replace('"', '\\"')
|
||||||
return sx_response(f'(~inline-view :value "{escaped}")')
|
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("/examples/api/oob")
|
@bp.get("/examples/api/oob")
|
||||||
async def api_oob():
|
async def api_oob():
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
from sxc.sx_components import _oob_code, _full_wire_text
|
||||||
now = datetime.now().strftime("%H:%M:%S")
|
now = datetime.now().strftime("%H:%M:%S")
|
||||||
return sx_response(
|
sx_src = (
|
||||||
f'(<>'
|
f'(<>'
|
||||||
f' (p :class "text-emerald-600 font-medium" "Box A updated!")'
|
f' (p :class "text-emerald-600 font-medium" "Box A updated!")'
|
||||||
f' (p :class "text-sm text-stone-500" "at {now}")'
|
f' (p :class "text-sm text-stone-500" "at {now}")'
|
||||||
@@ -191,6 +236,9 @@ def register(url_prefix: str = "/") -> Blueprint:
|
|||||||
f' (p :class "text-violet-600 font-medium" "Box B updated via OOB!")'
|
f' (p :class "text-violet-600 font-medium" "Box B updated via OOB!")'
|
||||||
f' (p :class "text-sm text-stone-500" "at {now}")))'
|
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})')
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Essays
|
# Essays
|
||||||
|
|||||||
@@ -28,10 +28,11 @@
|
|||||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors"
|
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors"
|
||||||
"Load content")))
|
"Load content")))
|
||||||
|
|
||||||
(defcomp ~click-result ()
|
(defcomp ~click-result (&key time)
|
||||||
(div :class "space-y-2"
|
(div :class "space-y-2"
|
||||||
(p :class "text-stone-800 font-medium" "Content loaded!")
|
(p :class "text-stone-800 font-medium" "Content loaded!")
|
||||||
(p :class "text-stone-500 text-sm" "This was fetched from the server via sx-get and swapped into the target div.")))
|
(p :class "text-stone-500 text-sm"
|
||||||
|
(str "Fetched from the server via sx-get at " time))))
|
||||||
|
|
||||||
;; --- Form submission demo ---
|
;; --- Form submission demo ---
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,83 @@ def _code(code: str, language: str = "lisp") -> str:
|
|||||||
return f'(~doc-code :code {highlighted})'
|
return f'(~doc-code :code {highlighted})'
|
||||||
|
|
||||||
|
|
||||||
def _example_code(code: str) -> str:
|
def _example_code(code: str, language: str = "lisp") -> str:
|
||||||
"""Build an ~example-source component with highlighted content."""
|
"""Build an ~example-source component with highlighted content."""
|
||||||
highlighted = highlight(code, "lisp")
|
highlighted = highlight(code, language)
|
||||||
return f'(~example-source :code {highlighted})'
|
return f'(~example-source :code {highlighted})'
|
||||||
|
|
||||||
|
|
||||||
|
def _placeholder(div_id: str) -> str:
|
||||||
|
"""Empty placeholder that will be filled by OOB swap on interaction."""
|
||||||
|
return (f'(div :id "{div_id}"'
|
||||||
|
f' (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3"'
|
||||||
|
f' (p :class "text-stone-400 italic text-sm"'
|
||||||
|
f' "Trigger the demo to see the actual content.")))')
|
||||||
|
|
||||||
|
|
||||||
|
def _component_source_text(*names: str) -> str:
|
||||||
|
"""Get defcomp source text for named components."""
|
||||||
|
from shared.sx.jinja_bridge import _COMPONENT_ENV
|
||||||
|
from shared.sx.types import Component
|
||||||
|
from shared.sx.parser import serialize
|
||||||
|
parts = []
|
||||||
|
for name in names:
|
||||||
|
key = name if name.startswith("~") else f"~{name}"
|
||||||
|
val = _COMPONENT_ENV.get(key)
|
||||||
|
if isinstance(val, Component):
|
||||||
|
param_strs = ["&key"] + list(val.params)
|
||||||
|
if val.has_children:
|
||||||
|
param_strs.extend(["&rest", "children"])
|
||||||
|
params_sx = "(" + " ".join(param_strs) + ")"
|
||||||
|
body_sx = serialize(val.body, pretty=True)
|
||||||
|
parts.append(f"(defcomp ~{val.name} {params_sx}\n{body_sx})")
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _oob_code(target_id: str, text: str) -> str:
|
||||||
|
"""OOB swap that displays plain code in a styled block."""
|
||||||
|
escaped = text.replace('\\', '\\\\').replace('"', '\\"')
|
||||||
|
return (f'(div :id "{target_id}" :sx-swap-oob "innerHTML"'
|
||||||
|
f' (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3 overflow-x-auto"'
|
||||||
|
f' (pre :class "text-sm whitespace-pre-wrap"'
|
||||||
|
f' (code "{escaped}"))))')
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_components_btn() -> str:
|
||||||
|
"""Button that clears the client-side component cache (localStorage + in-memory)."""
|
||||||
|
js = ("localStorage.removeItem('sx-components-hash');"
|
||||||
|
"localStorage.removeItem('sx-components-src');"
|
||||||
|
"var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});"
|
||||||
|
"var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)")
|
||||||
|
return (f'(button :onclick "{js}"'
|
||||||
|
f' :class "text-xs text-stone-400 hover:text-stone-600 border border-stone-200'
|
||||||
|
f' rounded px-2 py-1 transition-colors"'
|
||||||
|
f' "Clear component cache")')
|
||||||
|
|
||||||
|
|
||||||
|
def _full_wire_text(sx_src: str, *comp_names: str) -> str:
|
||||||
|
"""Build the full wire response text showing component defs + CSS note + sx source.
|
||||||
|
|
||||||
|
Only includes component definitions the client doesn't already have,
|
||||||
|
matching the real behaviour of sx_response().
|
||||||
|
"""
|
||||||
|
from quart import request
|
||||||
|
parts = []
|
||||||
|
if comp_names:
|
||||||
|
# Check which components the client already has
|
||||||
|
loaded_raw = request.headers.get("SX-Components", "")
|
||||||
|
loaded = set(loaded_raw.split(",")) if loaded_raw else set()
|
||||||
|
missing = [n for n in comp_names
|
||||||
|
if f"~{n}" not in loaded and n not in loaded]
|
||||||
|
if missing:
|
||||||
|
comp_text = _component_source_text(*missing)
|
||||||
|
if comp_text:
|
||||||
|
parts.append(f'<script type="text/sx" data-components>\n{comp_text}\n</script>')
|
||||||
|
parts.append('<style data-sx-css>/* new CSS rules */</style>')
|
||||||
|
parts.append(sx_src)
|
||||||
|
return "\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Navigation helpers
|
# Navigation helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -835,11 +906,17 @@ def _examples_content_sx(slug: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _example_click_to_load_sx() -> str:
|
def _example_click_to_load_sx() -> str:
|
||||||
c1 = _example_code('(button\n'
|
c_sx = _example_code('(button\n'
|
||||||
' :sx-get "/examples/api/click"\n'
|
' :sx-get "/examples/api/click"\n'
|
||||||
' :sx-target "#click-result"\n'
|
' :sx-target "#click-result"\n'
|
||||||
' :sx-swap "innerHTML"\n'
|
' :sx-swap "innerHTML"\n'
|
||||||
' "Load content")')
|
' "Load content")')
|
||||||
|
c_handler = _example_code('@bp.get("/examples/api/click")\n'
|
||||||
|
'async def api_click():\n'
|
||||||
|
' now = datetime.now().strftime(...)\n'
|
||||||
|
' return sx_response(\n'
|
||||||
|
' f\'(~click-result :time "{now}")\')',
|
||||||
|
language="python")
|
||||||
return (
|
return (
|
||||||
f'(~doc-page :title "Click to Load"'
|
f'(~doc-page :title "Click to Load"'
|
||||||
f' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
@@ -847,17 +924,36 @@ def _example_click_to_load_sx() -> str:
|
|||||||
f' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
f' :description "Click the button to load server-rendered content."'
|
f' :description "Click the button to load server-rendered content."'
|
||||||
f' (~example-demo (~click-to-load-demo)))'
|
f' (~example-demo (~click-to-load-demo)))'
|
||||||
f' {c1})'
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||||
|
f' {c_sx}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||||
|
f' {_placeholder("click-comp")}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||||
|
f' {c_handler}'
|
||||||
|
f' (div :class "flex items-center justify-between mt-6"'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||||
|
f' {_clear_components_btn()})'
|
||||||
|
f' (p :class "text-stone-500 text-sm mb-2"'
|
||||||
|
f' "The server responds with content-type text/sx. New CSS rules are prepended as a style tag.'
|
||||||
|
f' Clear the component cache to see component definitions included in the wire response.")'
|
||||||
|
f' {_placeholder("click-wire")})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_form_submission_sx() -> str:
|
def _example_form_submission_sx() -> str:
|
||||||
c1 = _example_code('(form\n'
|
c_sx = _example_code('(form\n'
|
||||||
' :sx-post "/examples/api/form"\n'
|
' :sx-post "/examples/api/form"\n'
|
||||||
' :sx-target "#form-result"\n'
|
' :sx-target "#form-result"\n'
|
||||||
' :sx-swap "innerHTML"\n'
|
' :sx-swap "innerHTML"\n'
|
||||||
' (input :type "text" :name "name")\n'
|
' (input :type "text" :name "name")\n'
|
||||||
' (button :type "submit" "Submit"))')
|
' (button :type "submit" "Submit"))')
|
||||||
|
c_handler = _example_code('@bp.post("/examples/api/form")\n'
|
||||||
|
'async def api_form():\n'
|
||||||
|
' form = await request.form\n'
|
||||||
|
' name = form.get("name", "")\n'
|
||||||
|
' return sx_response(\n'
|
||||||
|
' f\'(~form-result :name "{name}")\')',
|
||||||
|
language="python")
|
||||||
return (
|
return (
|
||||||
f'(~doc-page :title "Form Submission"'
|
f'(~doc-page :title "Form Submission"'
|
||||||
f' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
@@ -865,16 +961,33 @@ def _example_form_submission_sx() -> str:
|
|||||||
f' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
f' :description "Enter a name and submit."'
|
f' :description "Enter a name and submit."'
|
||||||
f' (~example-demo (~form-demo)))'
|
f' (~example-demo (~form-demo)))'
|
||||||
f' {c1})'
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||||
|
f' {c_sx}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||||
|
f' {_placeholder("form-comp")}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||||
|
f' {c_handler}'
|
||||||
|
f' (div :class "flex items-center justify-between mt-6"'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||||
|
f' {_clear_components_btn()})'
|
||||||
|
f' {_placeholder("form-wire")})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_polling_sx() -> str:
|
def _example_polling_sx() -> str:
|
||||||
c1 = _example_code('(div\n'
|
c_sx = _example_code('(div\n'
|
||||||
' :sx-get "/examples/api/poll"\n'
|
' :sx-get "/examples/api/poll"\n'
|
||||||
' :sx-trigger "load, every 2s"\n'
|
' :sx-trigger "load, every 2s"\n'
|
||||||
' :sx-swap "innerHTML"\n'
|
' :sx-swap "innerHTML"\n'
|
||||||
' "Loading...")')
|
' "Loading...")')
|
||||||
|
c_handler = _example_code('@bp.get("/examples/api/poll")\n'
|
||||||
|
'async 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})\')',
|
||||||
|
language="python")
|
||||||
return (
|
return (
|
||||||
f'(~doc-page :title "Polling"'
|
f'(~doc-page :title "Polling"'
|
||||||
f' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
@@ -882,19 +995,36 @@ def _example_polling_sx() -> str:
|
|||||||
f' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
f' :description "This div polls the server every 2 seconds."'
|
f' :description "This div polls the server every 2 seconds."'
|
||||||
f' (~example-demo (~polling-demo)))'
|
f' (~example-demo (~polling-demo)))'
|
||||||
f' {c1})'
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||||
|
f' {c_sx}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||||
|
f' {_placeholder("poll-comp")}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||||
|
f' {c_handler}'
|
||||||
|
f' (div :class "flex items-center justify-between mt-6"'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||||
|
f' {_clear_components_btn()})'
|
||||||
|
f' (p :class "text-stone-500 text-sm mb-2"'
|
||||||
|
f' "Updates every 2 seconds — watch the time and count change.")'
|
||||||
|
f' {_placeholder("poll-wire")})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_delete_row_sx() -> str:
|
def _example_delete_row_sx() -> str:
|
||||||
from content.pages import DELETE_DEMO_ITEMS
|
from content.pages import DELETE_DEMO_ITEMS
|
||||||
items_sx = " ".join(f'(list "{id}" "{name}")' for id, name in DELETE_DEMO_ITEMS)
|
items_sx = " ".join(f'(list "{id}" "{name}")' for id, name in DELETE_DEMO_ITEMS)
|
||||||
c1 = _example_code('(button\n'
|
c_sx = _example_code('(button\n'
|
||||||
' :sx-delete "/api/delete/1"\n'
|
' :sx-delete "/api/delete/1"\n'
|
||||||
' :sx-target "#row-1"\n'
|
' :sx-target "#row-1"\n'
|
||||||
' :sx-swap "outerHTML"\n'
|
' :sx-swap "outerHTML"\n'
|
||||||
' :sx-confirm "Delete this item?"\n'
|
' :sx-confirm "Delete this item?"\n'
|
||||||
' "delete")')
|
' "delete")')
|
||||||
|
c_handler = _example_code('@bp.delete("/examples/api/delete/<item_id>")\n'
|
||||||
|
'async def api_delete(item_id: str):\n'
|
||||||
|
' # Empty response — outerHTML swap removes the row\n'
|
||||||
|
' return Response("", status=200,\n'
|
||||||
|
' content_type="text/sx")',
|
||||||
|
language="python")
|
||||||
return (
|
return (
|
||||||
f'(~doc-page :title "Delete Row"'
|
f'(~doc-page :title "Delete Row"'
|
||||||
f' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
@@ -902,20 +1032,38 @@ def _example_delete_row_sx() -> str:
|
|||||||
f' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
f' :description "Click delete to remove a row. Uses sx-confirm for confirmation."'
|
f' :description "Click delete to remove a row. Uses sx-confirm for confirmation."'
|
||||||
f' (~example-demo (~delete-demo :items (list {items_sx}))))'
|
f' (~example-demo (~delete-demo :items (list {items_sx}))))'
|
||||||
f' {c1})'
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||||
|
f' {c_sx}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
|
||||||
|
f' {_placeholder("delete-comp")}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||||
|
f' {c_handler}'
|
||||||
|
f' (div :class "flex items-center justify-between mt-6"'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||||
|
f' {_clear_components_btn()})'
|
||||||
|
f' (p :class "text-stone-500 text-sm mb-2"'
|
||||||
|
f' "Empty body — outerHTML swap replaces the target element with nothing.")'
|
||||||
|
f' {_placeholder("delete-wire")})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_inline_edit_sx() -> str:
|
def _example_inline_edit_sx() -> str:
|
||||||
c1 = _example_code(';; View mode\n'
|
c_sx = _example_code(';; View mode — shows text + edit button\n'
|
||||||
'(button :sx-get "/api/edit?value=text"\n'
|
'(~inline-view :value "some text")\n\n'
|
||||||
' :sx-target "#edit-target" :sx-swap "innerHTML"\n'
|
';; Edit mode — returned by server on click\n'
|
||||||
' "edit")\n\n'
|
'(~inline-edit-form :value "some text")')
|
||||||
';; Edit mode (returned by server)\n'
|
c_handler = _example_code('@bp.get("/examples/api/edit")\n'
|
||||||
'(form :sx-post "/api/edit"\n'
|
'async def api_edit_form():\n'
|
||||||
' :sx-target "#edit-target" :sx-swap "innerHTML"\n'
|
' value = request.args.get("value", "")\n'
|
||||||
' (input :type "text" :name "value")\n'
|
' return sx_response(\n'
|
||||||
' (button :type "submit" "save"))')
|
' f\'(~inline-edit-form :value "{value}")\')\n\n'
|
||||||
|
'@bp.post("/examples/api/edit")\n'
|
||||||
|
'async def api_edit_save():\n'
|
||||||
|
' form = await request.form\n'
|
||||||
|
' value = form.get("value", "")\n'
|
||||||
|
' return sx_response(\n'
|
||||||
|
' f\'(~inline-view :value "{value}")\')',
|
||||||
|
language="python")
|
||||||
return (
|
return (
|
||||||
f'(~doc-page :title "Inline Edit"'
|
f'(~doc-page :title "Inline Edit"'
|
||||||
f' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
@@ -923,18 +1071,36 @@ def _example_inline_edit_sx() -> str:
|
|||||||
f' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
f' :description "Click edit, modify the text, save or cancel."'
|
f' :description "Click edit, modify the text, save or cancel."'
|
||||||
f' (~example-demo (~inline-edit-demo)))'
|
f' (~example-demo (~inline-edit-demo)))'
|
||||||
f' {c1})'
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||||
|
f' {c_sx}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Components")'
|
||||||
|
f' {_placeholder("edit-comp")}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handlers")'
|
||||||
|
f' {c_handler}'
|
||||||
|
f' (div :class "flex items-center justify-between mt-6"'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||||
|
f' {_clear_components_btn()})'
|
||||||
|
f' {_placeholder("edit-wire")})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _example_oob_swaps_sx() -> str:
|
def _example_oob_swaps_sx() -> str:
|
||||||
c1 = _example_code(';; Response body updates the target (Box A)\n'
|
c_sx = _example_code(';; Button targets Box A\n'
|
||||||
';; OOB element updates Box B by ID\n\n'
|
'(button\n'
|
||||||
'(<>\n'
|
' :sx-get "/examples/api/oob"\n'
|
||||||
' (div :class "text-center"\n'
|
' :sx-target "#oob-box-a"\n'
|
||||||
' (p "Box A updated!")))\n'
|
' :sx-swap "innerHTML"\n'
|
||||||
' (div :id "oob-box-b" :sx-swap-oob "innerHTML"\n'
|
' "Update both boxes")')
|
||||||
' (p "Box B updated via OOB!")))')
|
c_handler = _example_code('@bp.get("/examples/api/oob")\n'
|
||||||
|
'async 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}")))\')',
|
||||||
|
language="python")
|
||||||
return (
|
return (
|
||||||
f'(~doc-page :title "Out-of-Band Swaps"'
|
f'(~doc-page :title "Out-of-Band Swaps"'
|
||||||
f' (p :class "text-stone-600 mb-6"'
|
f' (p :class "text-stone-600 mb-6"'
|
||||||
@@ -942,7 +1108,16 @@ def _example_oob_swaps_sx() -> str:
|
|||||||
f' (~example-card :title "Demo"'
|
f' (~example-card :title "Demo"'
|
||||||
f' :description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."'
|
f' :description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."'
|
||||||
f' (~example-demo (~oob-demo)))'
|
f' (~example-demo (~oob-demo)))'
|
||||||
f' {c1})'
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
|
||||||
|
f' {c_sx}'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
|
||||||
|
f' {c_handler}'
|
||||||
|
f' (div :class "flex items-center justify-between mt-6"'
|
||||||
|
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
|
||||||
|
f' {_clear_components_btn()})'
|
||||||
|
f' (p :class "text-stone-500 text-sm mb-2"'
|
||||||
|
f' "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.")'
|
||||||
|
f' {_placeholder("oob-wire")})'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user