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: [
|
||||
// ~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)
|
||||
{ pattern: /^bg-sky-(100|200|300|400)$/ },
|
||||
{ pattern: /^bg-sky-(100|200|300|400|500)$/ },
|
||||
{ pattern: /^bg-violet-(100|200|300|400|500)$/ },
|
||||
],
|
||||
content: [
|
||||
'/root/rose-ash/shared/sx/templates/**/*.sx',
|
||||
@@ -30,6 +31,9 @@ module.exports = {
|
||||
'/root/rose-ash/federation/sx/sx_components.py',
|
||||
'/root/rose-ash/account/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: {
|
||||
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
|
||||
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()
|
||||
cumulative_classes: set[str] = set()
|
||||
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.)
|
||||
new_classes.update(HELPER_CSS_CLASSES)
|
||||
if comp_defs:
|
||||
# Use pre-computed classes for components being sent
|
||||
for key, val in _COMPONENT_ENV.items():
|
||||
if isinstance(val, _Component) and val.css_classes:
|
||||
new_classes.update(val.css_classes)
|
||||
# Scan only the component definitions actually being sent
|
||||
new_classes.update(scan_classes_from_sx(comp_defs))
|
||||
|
||||
# Resolve known classes from SX-Css header (hash or full list)
|
||||
known_classes: set[str] = set()
|
||||
|
||||
@@ -132,58 +132,103 @@ def register(url_prefix: str = "/") -> Blueprint:
|
||||
@bp.get("/examples/api/click")
|
||||
async def api_click():
|
||||
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")
|
||||
async def api_form():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
name = form.get("name", "")
|
||||
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}
|
||||
|
||||
@bp.get("/examples/api/poll")
|
||||
async def api_poll():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components 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)
|
||||
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>")
|
||||
async def api_delete(item_id: str):
|
||||
# Return empty response — the row's outerHTML swap removes it
|
||||
return Response("", status=200, content_type="text/sx")
|
||||
from shared.sx.helpers import sx_response
|
||||
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")
|
||||
async def api_edit_form():
|
||||
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", "")
|
||||
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")
|
||||
async def api_edit_save():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _component_source_text, _full_wire_text
|
||||
form = await request.form
|
||||
value = form.get("value", "")
|
||||
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")
|
||||
async def api_edit_cancel():
|
||||
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", "")
|
||||
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")
|
||||
async def api_oob():
|
||||
from shared.sx.helpers import sx_response
|
||||
from sxc.sx_components import _oob_code, _full_wire_text
|
||||
now = datetime.now().strftime("%H:%M:%S")
|
||||
return sx_response(
|
||||
sx_src = (
|
||||
f'(<>'
|
||||
f' (p :class "text-emerald-600 font-medium" "Box A updated!")'
|
||||
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-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
|
||||
|
||||
@@ -28,10 +28,11 @@
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors"
|
||||
"Load content")))
|
||||
|
||||
(defcomp ~click-result ()
|
||||
(defcomp ~click-result (&key time)
|
||||
(div :class "space-y-2"
|
||||
(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 ---
|
||||
|
||||
|
||||
@@ -28,12 +28,83 @@ def _code(code: str, language: str = "lisp") -> str:
|
||||
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."""
|
||||
highlighted = highlight(code, "lisp")
|
||||
highlighted = highlight(code, language)
|
||||
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
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -835,11 +906,17 @@ def _examples_content_sx(slug: str) -> str:
|
||||
|
||||
|
||||
def _example_click_to_load_sx() -> str:
|
||||
c1 = _example_code('(button\n'
|
||||
' :sx-get "/examples/api/click"\n'
|
||||
' :sx-target "#click-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Load content")')
|
||||
c_sx = _example_code('(button\n'
|
||||
' :sx-get "/examples/api/click"\n'
|
||||
' :sx-target "#click-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "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 (
|
||||
f'(~doc-page :title "Click to Load"'
|
||||
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' :description "Click the button to load server-rendered content."'
|
||||
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:
|
||||
c1 = _example_code('(form\n'
|
||||
' :sx-post "/examples/api/form"\n'
|
||||
' :sx-target "#form-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' (input :type "text" :name "name")\n'
|
||||
' (button :type "submit" "Submit"))')
|
||||
c_sx = _example_code('(form\n'
|
||||
' :sx-post "/examples/api/form"\n'
|
||||
' :sx-target "#form-result"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' (input :type "text" :name "name")\n'
|
||||
' (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 (
|
||||
f'(~doc-page :title "Form Submission"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
@@ -865,16 +961,33 @@ def _example_form_submission_sx() -> str:
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Enter a name and submit."'
|
||||
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:
|
||||
c1 = _example_code('(div\n'
|
||||
' :sx-get "/examples/api/poll"\n'
|
||||
' :sx-trigger "load, every 2s"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Loading...")')
|
||||
c_sx = _example_code('(div\n'
|
||||
' :sx-get "/examples/api/poll"\n'
|
||||
' :sx-trigger "load, every 2s"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "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 (
|
||||
f'(~doc-page :title "Polling"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
@@ -882,19 +995,36 @@ def _example_polling_sx() -> str:
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "This div polls the server every 2 seconds."'
|
||||
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:
|
||||
from content.pages import DELETE_DEMO_ITEMS
|
||||
items_sx = " ".join(f'(list "{id}" "{name}")' for id, name in DELETE_DEMO_ITEMS)
|
||||
c1 = _example_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")')
|
||||
c_sx = _example_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")')
|
||||
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 (
|
||||
f'(~doc-page :title "Delete Row"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
@@ -902,20 +1032,38 @@ def _example_delete_row_sx() -> str:
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click delete to remove a row. Uses sx-confirm for confirmation."'
|
||||
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:
|
||||
c1 = _example_code(';; View mode\n'
|
||||
'(button :sx-get "/api/edit?value=text"\n'
|
||||
' :sx-target "#edit-target" :sx-swap "innerHTML"\n'
|
||||
' "edit")\n\n'
|
||||
';; Edit mode (returned by server)\n'
|
||||
'(form :sx-post "/api/edit"\n'
|
||||
' :sx-target "#edit-target" :sx-swap "innerHTML"\n'
|
||||
' (input :type "text" :name "value")\n'
|
||||
' (button :type "submit" "save"))')
|
||||
c_sx = _example_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")')
|
||||
c_handler = _example_code('@bp.get("/examples/api/edit")\n'
|
||||
'async 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("/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 (
|
||||
f'(~doc-page :title "Inline Edit"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
@@ -923,18 +1071,36 @@ def _example_inline_edit_sx() -> str:
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "Click edit, modify the text, save or cancel."'
|
||||
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:
|
||||
c1 = _example_code(';; Response body updates the target (Box A)\n'
|
||||
';; OOB element updates Box B by ID\n\n'
|
||||
'(<>\n'
|
||||
' (div :class "text-center"\n'
|
||||
' (p "Box A updated!")))\n'
|
||||
' (div :id "oob-box-b" :sx-swap-oob "innerHTML"\n'
|
||||
' (p "Box B updated via OOB!")))')
|
||||
c_sx = _example_code(';; Button targets Box A\n'
|
||||
'(button\n'
|
||||
' :sx-get "/examples/api/oob"\n'
|
||||
' :sx-target "#oob-box-a"\n'
|
||||
' :sx-swap "innerHTML"\n'
|
||||
' "Update both boxes")')
|
||||
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 (
|
||||
f'(~doc-page :title "Out-of-Band Swaps"'
|
||||
f' (p :class "text-stone-600 mb-6"'
|
||||
@@ -942,7 +1108,16 @@ def _example_oob_swaps_sx() -> str:
|
||||
f' (~example-card :title "Demo"'
|
||||
f' :description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."'
|
||||
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