Replace JSON sx-headers with SX dict expressions, fix blog like component
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m6s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m6s
sx-headers attributes now use native SX dict format {:key val} instead of
JSON strings. Eliminates manual JSON string construction in both .sx files
and Python callers.
- sx.js: parse sx-headers/sx-vals as SX dict ({: prefix) with JSON fallback,
add _serializeDict for dict→attribute serialization, fix verbInfo scope in
_doFetch error handler
- html.py: serialize dict attribute values via SX serialize() not str()
- All .sx files: {:X-CSRFToken csrf} replaces (str "{\"X-CSRFToken\": ...}")
- All Python callers: {"X-CSRFToken": csrf} dict replaces f-string JSON
- Blog like: extract ~blog-like-toggle, fix POST returning wrong component,
fix emoji escapes in .sx (parser has no \U support), fix card :hx-headers
keyword mismatch, wrap sx_content in SxExpr for evaluation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,7 @@ def register(url_prefix="/"):
|
||||
return sx_response(sx_call(
|
||||
"account-newsletter-toggle",
|
||||
id=f"nl-{nid}", url=toggle_url,
|
||||
hdrs=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
hdrs={"X-CSRFToken": csrf},
|
||||
target=f"#nl-{nid}",
|
||||
cls=f"relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 {bg}",
|
||||
checked=checked,
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
:toggle (~account-newsletter-toggle
|
||||
:id (str "nl-" nid)
|
||||
:url toggle-url
|
||||
:hdrs (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:hdrs {:X-CSRFToken csrf}
|
||||
:target (str "#nl-" nid)
|
||||
:cls (str "relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2 " bg)
|
||||
:checked checked
|
||||
|
||||
@@ -138,7 +138,7 @@ def _render_calendar_view(
|
||||
e_id = getattr(e, "id", None)
|
||||
e_name = esc(getattr(e, "name", ""))
|
||||
t_url = toggle_url_fn(e_id)
|
||||
hx_hdrs = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
hx_hdrs = '{:X-CSRFToken "' + csrf + '"}'
|
||||
|
||||
if e_id in associated_entry_ids:
|
||||
entry_btns.append(
|
||||
|
||||
@@ -156,14 +156,10 @@ def register():
|
||||
csrf = generate_csrf_token()
|
||||
|
||||
def _like_btn(liked):
|
||||
if liked:
|
||||
colour, icon, label = "text-red-600", "fa-solid fa-heart", "Unlike this post"
|
||||
else:
|
||||
colour, icon, label = "text-stone-300", "fa-regular fa-heart", "Like this post"
|
||||
return sx_call("market-like-toggle-button",
|
||||
colour=colour, action=like_url,
|
||||
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
label=label, icon_cls=icon)
|
||||
return sx_call("blog-like-toggle",
|
||||
like_url=like_url,
|
||||
hx_headers={"X-CSRFToken": csrf},
|
||||
heart="\u2764\ufe0f" if liked else "\U0001f90d")
|
||||
|
||||
if not g.user:
|
||||
return sx_response(_like_btn(False), status=403)
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
"""Blog page data service — provides serialized dicts for .sx defpages."""
|
||||
from __future__ import annotations
|
||||
|
||||
from shared.sx.parser import SxExpr
|
||||
|
||||
|
||||
def _sx_content_expr(raw: str) -> SxExpr | None:
|
||||
"""Wrap non-empty sx_content as SxExpr so it serializes unquoted."""
|
||||
return SxExpr(raw) if raw else None
|
||||
|
||||
|
||||
class BlogPageService:
|
||||
"""Service for blog page data, callable via (service "blog-page" ...)."""
|
||||
@@ -424,7 +431,7 @@ class BlogPageService:
|
||||
"authors": authors,
|
||||
"feature_image": post.get("feature_image"),
|
||||
"html_content": post.get("html", ""),
|
||||
"sx_content": post.get("sx_content", ""),
|
||||
"sx_content": _sx_content_expr(post.get("sx_content", "")),
|
||||
}
|
||||
|
||||
async def preview_data(self, session, *, slug=None, **kw):
|
||||
|
||||
@@ -206,7 +206,7 @@
|
||||
(when is-admin
|
||||
(~blog-snippet-visibility-select
|
||||
:patch-url (get s "patch_url")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:hx-headers {:X-CSRFToken csrf}
|
||||
:options (<>
|
||||
(~blog-snippet-option :value "private" :selected (= vis "private") :label "private")
|
||||
(~blog-snippet-option :value "shared" :selected (= vis "shared") :label "shared")
|
||||
@@ -217,7 +217,7 @@
|
||||
:trigger-target "#snippets-list"
|
||||
:title "Delete snippet?"
|
||||
:text (str "Delete \u201c" name "\u201d?")
|
||||
:sx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:sx-headers {:X-CSRFToken csrf}
|
||||
:cls "px-3 py-1 text-sm bg-red-200 hover:bg-red-300 rounded text-red-800 flex-shrink-0"))))))
|
||||
(or snippets (list)))))))
|
||||
|
||||
@@ -240,7 +240,7 @@
|
||||
:edit-url (get mi "edit_url")
|
||||
:delete-url (get mi "delete_url")
|
||||
:confirm-text (str "Remove " (get mi "label") " from the menu?")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")))
|
||||
:hx-headers {:X-CSRFToken csrf}))
|
||||
(or menu-items (list)))))))
|
||||
|
||||
;; Tag Groups — receives serialized tag group data from service
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
(defcomp ~blog-like-button (&key like-url hx-headers heart)
|
||||
(div :class "absolute top-20 right-2 z-10 text-6xl md:text-4xl"
|
||||
(button :sx-post like-url :sx-swap "outerHTML"
|
||||
:sx-headers hx-headers :class "cursor-pointer" heart)))
|
||||
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
|
||||
|
||||
(defcomp ~blog-draft-status (&key publish-requested timestamp)
|
||||
(<> (div :class "flex justify-center gap-2 mt-1"
|
||||
@@ -56,7 +55,7 @@
|
||||
(when has-like
|
||||
(~blog-like-button
|
||||
:like-url like-url
|
||||
:sx-headers (str "{\"X-CSRFToken\": \"" csrf-token "\"}")
|
||||
:hx-headers {:X-CSRFToken csrf-token}
|
||||
:heart (if liked "❤️" "🤍")))
|
||||
(a :href href :sx-get href :sx-target "#main-panel"
|
||||
:sx-select (or hx-select "#main-panel") :sx-swap "outerHTML" :sx-push-url "true"
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
(when publish-requested (span :class "inline-block px-3 py-1 rounded-full text-sm font-semibold bg-blue-100 text-blue-800" "Publish requested"))
|
||||
edit))
|
||||
|
||||
(defcomp ~blog-like-toggle (&key like-url hx-headers heart)
|
||||
(button :sx-post like-url :sx-swap "outerHTML"
|
||||
:sx-headers hx-headers :class "cursor-pointer" heart))
|
||||
|
||||
(defcomp ~blog-detail-like (&key like-url hx-headers heart)
|
||||
(div :class "absolute top-2 right-2 z-10 text-8xl md:text-6xl"
|
||||
(button :sx-post like-url :sx-swap "outerHTML"
|
||||
:sx-headers hx-headers :class "cursor-pointer" heart)))
|
||||
(~blog-like-toggle :like-url like-url :hx-headers hx-headers :heart heart)))
|
||||
|
||||
(defcomp ~blog-detail-excerpt (&key excerpt)
|
||||
(div :class "w-full text-center italic text-3xl p-2" excerpt))
|
||||
@@ -55,8 +58,8 @@
|
||||
:like (when has-user
|
||||
(~blog-detail-like
|
||||
:like-url like-url
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:heart (if liked "\u2764\ufe0f" "\U0001f90d")))
|
||||
:hx-headers {:X-CSRFToken csrf}
|
||||
:heart (if liked "❤️" "🤍")))
|
||||
:excerpt (when (not (= custom-excerpt ""))
|
||||
(~blog-detail-excerpt :excerpt custom-excerpt))
|
||||
:at-bar (~blog-at-bar :tags tags :authors authors)))))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
(defcomp ~blog-features-form (&key features-url calendar-checked market-checked hs-trigger)
|
||||
(form :sx-put features-url :sx-target "#features-panel" :sx-swap "outerHTML"
|
||||
:sx-headers "{\"Content-Type\": \"application/json\"}" :sx-encoding "json" :class "space-y-3"
|
||||
:sx-headers {:Content-Type "application/json"} :sx-encoding "json" :class "space-y-3"
|
||||
(label :class "flex items-center gap-3 cursor-pointer"
|
||||
(input :type "checkbox" :name "calendar" :value "true" :checked calendar-checked
|
||||
:class "h-5 w-5 rounded border-stone-300 text-blue-600 focus:ring-blue-500"
|
||||
@@ -140,7 +140,7 @@
|
||||
(~blog-associated-entry
|
||||
:confirm-text (get e "confirm_text")
|
||||
:toggle-url (get e "toggle_url")
|
||||
:hx-headers (str "{\"X-CSRFToken\": \"" csrf "\"}")
|
||||
:hx-headers {:X-CSRFToken csrf}
|
||||
:img (~blog-entry-image :src (get e "cal_image") :title (get e "cal_title"))
|
||||
:name (get e "name")
|
||||
:date-str (get e "date_str")))
|
||||
|
||||
@@ -323,7 +323,7 @@ def _calendars_list_sx(ctx: dict, calendars: list) -> str:
|
||||
cal_name = getattr(cal, "name", "")
|
||||
href = prefix + url_for("calendar.get", calendar_slug=cal_slug)
|
||||
del_url = url_for("calendar.delete", calendar_slug=cal_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
csrf_hdr = {"X-CSRFToken": csrf}
|
||||
parts.append(sx_call("crud-item",
|
||||
href=href, name=cal_name, slug=cal_slug,
|
||||
del_url=del_url, csrf_hdr=csrf_hdr,
|
||||
@@ -656,7 +656,7 @@ def _markets_list_html(ctx: dict, markets: list) -> str:
|
||||
m_name = getattr(m, "name", "") if hasattr(m, "name") else m.get("name", "")
|
||||
market_href = call_url(ctx, "market_url", f"/{slug}/{m_slug}/")
|
||||
del_url = url_for("markets.delete_market", market_slug=m_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
csrf_hdr = {"X-CSRFToken": csrf}
|
||||
parts.append(sx_call("crud-item",
|
||||
href=market_href, name=m_name,
|
||||
slug=m_slug, del_url=del_url,
|
||||
|
||||
@@ -552,7 +552,7 @@ def render_entry_posts_panel(entry_posts, entry, calendar, day, month, year) ->
|
||||
items += sx_call("events-entry-post-item",
|
||||
img=img_html, title=ep_title,
|
||||
del_url=del_url, entry_id=eid_s,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
csrf_hdr={"X-CSRFToken": csrf})
|
||||
posts_html = sx_call("events-entry-posts-list", items=SxExpr(items))
|
||||
else:
|
||||
posts_html = sx_call("events-entry-posts-none")
|
||||
|
||||
@@ -387,7 +387,7 @@ async def _h_slots_data(calendar_slug=None, **kw) -> dict:
|
||||
csrf = generate_csrf_token()
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
csrf_hdr = {"X-CSRFToken": csrf}
|
||||
add_url = url_for("calendar.slots.add_form", calendar_slug=cal_slug)
|
||||
|
||||
slots_list = []
|
||||
@@ -624,7 +624,7 @@ async def _h_ticket_types_data(calendar_slug=None, entry_id=None,
|
||||
cal_slug = getattr(calendar, "slug", "")
|
||||
hx_select = getattr(g, "hx_select_search", "#main-panel")
|
||||
eid = entry.id if entry else 0
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
csrf_hdr = {"X-CSRFToken": csrf}
|
||||
|
||||
types_list = []
|
||||
for tt in (ticket_types or []):
|
||||
@@ -964,7 +964,7 @@ async def _h_markets_data(**kw) -> dict:
|
||||
|
||||
post = ctx.get("post") or {}
|
||||
slug = post.get("slug", "")
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
csrf_hdr = {"X-CSRFToken": csrf}
|
||||
|
||||
markets_list = []
|
||||
for m in markets_raw:
|
||||
|
||||
@@ -263,7 +263,7 @@ def render_slots_table(slots, calendar) -> str:
|
||||
time_str=f"{time_start} - {time_end}",
|
||||
cost_str=cost_str, action_btn=action_btn,
|
||||
del_url=del_url,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
csrf_hdr={"X-CSRFToken": csrf})
|
||||
else:
|
||||
rows_html = sx_call("events-slots-empty-row")
|
||||
|
||||
@@ -343,7 +343,7 @@ def render_slot_add_form(calendar) -> str:
|
||||
|
||||
post_url = url_for("calendar.slots.post", calendar_slug=cal_slug)
|
||||
cancel_url = url_for("calendar.slots.add_button", calendar_slug=cal_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
csrf_hdr = {"X-CSRFToken": csrf}
|
||||
|
||||
# Days checkboxes (all unchecked for add)
|
||||
day_keys = [("mon", "Mon"), ("tue", "Tue"), ("wed", "Wed"), ("thu", "Thu"),
|
||||
|
||||
@@ -419,7 +419,7 @@ def render_ticket_types_table(ticket_types, entry, calendar, day, month, year) -
|
||||
tt_name=tt.name, cost_str=cost_str,
|
||||
count=str(tt.count), action_btn=action_btn,
|
||||
del_url=del_url,
|
||||
csrf_hdr=f'{{"X-CSRFToken": "{csrf}"}}')
|
||||
csrf_hdr={"X-CSRFToken": csrf})
|
||||
else:
|
||||
rows_html = sx_call("events-ticket-types-empty-row")
|
||||
|
||||
@@ -699,7 +699,7 @@ def render_ticket_type_add_form(entry, calendar, day, month, year) -> str:
|
||||
cancel_url = url_for("calendar.day.calendar_entries.calendar_entry.ticket_types.add_button",
|
||||
calendar_slug=cal_slug, entry_id=entry.id,
|
||||
year=year, month=month, day=day)
|
||||
csrf_hdr = f'{{"X-CSRFToken": "{csrf}"}}'
|
||||
csrf_hdr = {"X-CSRFToken": csrf}
|
||||
|
||||
return sx_call("events-ticket-type-add-form",
|
||||
post_url=post_url, csrf=csrf_hdr,
|
||||
|
||||
@@ -129,7 +129,7 @@ async def _h_page_admin_data(slug=None, **kw) -> dict:
|
||||
m_name = getattr(m, "name", "") or (m.get("name", "") if isinstance(m, dict) else "")
|
||||
href = prefix + f"/{post_slug}/{m_slug}/"
|
||||
del_url = url_for("page_admin.delete_market", market_slug=m_slug)
|
||||
csrf_hdr = f'{{"X-CSRFToken":"{csrf}"}}'
|
||||
csrf_hdr = {"X-CSRFToken": csrf}
|
||||
markets.append({
|
||||
"href": href, "name": m_name, "slug": m_slug,
|
||||
"del-url": del_url, "csrf-hdr": csrf_hdr,
|
||||
|
||||
@@ -189,7 +189,7 @@ def render_like_toggle_button(slug: str, liked: bool, *,
|
||||
return sx_call(
|
||||
"market-like-toggle-button",
|
||||
colour=colour, action=like_url,
|
||||
hx_headers=f'{{"X-CSRFToken": "{csrf}"}}',
|
||||
hx_headers={"X-CSRFToken": csrf},
|
||||
label=label, icon_cls=icon,
|
||||
)
|
||||
|
||||
|
||||
@@ -251,6 +251,18 @@
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Serialize a JS object as SX dict {:key "val" ...} for attribute values. */
|
||||
function _serializeDict(obj) {
|
||||
var parts = [];
|
||||
for (var k in obj) {
|
||||
if (!obj.hasOwnProperty(k)) continue;
|
||||
var v = obj[k];
|
||||
var vs = typeof v === "string" ? '"' + v.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"' : String(v);
|
||||
parts.push(":" + k + " " + vs);
|
||||
}
|
||||
return "{" + parts.join(" ") + "}";
|
||||
}
|
||||
|
||||
// --- Primitives ---
|
||||
|
||||
var PRIMITIVES = {};
|
||||
@@ -1420,7 +1432,7 @@
|
||||
} else if (attrVal === true) {
|
||||
el.setAttribute(attrName, "");
|
||||
} else {
|
||||
el.setAttribute(attrName, String(attrVal));
|
||||
el.setAttribute(attrName, typeof attrVal === "object" && attrVal !== null && !Array.isArray(attrVal) ? _serializeDict(attrVal) : String(attrVal));
|
||||
}
|
||||
} else {
|
||||
// Child
|
||||
@@ -1851,7 +1863,7 @@
|
||||
cancelButtonText: "Cancel"
|
||||
}).then(function (result) {
|
||||
if (!result.isConfirmed) return;
|
||||
return _doFetch(el, method, url, extraParams);
|
||||
return _doFetch(el, verbInfo, method, url, extraParams);
|
||||
});
|
||||
}
|
||||
if (!window.confirm(confirmMsg)) return Promise.resolve();
|
||||
@@ -1866,10 +1878,10 @@
|
||||
extraParams.promptValue = promptVal;
|
||||
}
|
||||
|
||||
return _doFetch(el, method, url, extraParams);
|
||||
return _doFetch(el, verbInfo, method, url, extraParams);
|
||||
}
|
||||
|
||||
function _doFetch(el, method, url, extraParams) {
|
||||
function _doFetch(el, verbInfo, method, url, extraParams) {
|
||||
// sx-sync: abort previous
|
||||
var sync = el.getAttribute("sx-sync");
|
||||
if (sync && sync.indexOf("replace") >= 0) abortPrevious(el);
|
||||
@@ -1895,12 +1907,12 @@
|
||||
var cssHeader = _getSxCssHeader();
|
||||
if (cssHeader) headers["SX-Css"] = cssHeader;
|
||||
|
||||
// Extra headers from sx-headers
|
||||
// Extra headers from sx-headers (SX dict {:key "val"} or JSON)
|
||||
var extraH = el.getAttribute("sx-headers");
|
||||
if (extraH) {
|
||||
try {
|
||||
var parsed = JSON.parse(extraH);
|
||||
for (var k in parsed) headers[k] = parsed[k];
|
||||
var parsed = extraH.charAt(0) === "{" && extraH.charAt(1) === ":" ? parse(extraH) : JSON.parse(extraH);
|
||||
for (var k in parsed) headers[k] = String(parsed[k]);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
@@ -1974,7 +1986,7 @@
|
||||
var valsAttr = el.getAttribute("sx-vals");
|
||||
if (valsAttr) {
|
||||
try {
|
||||
var vals = JSON.parse(valsAttr);
|
||||
var vals = valsAttr.charAt(0) === "{" && valsAttr.charAt(1) === ":" ? parse(valsAttr) : JSON.parse(valsAttr);
|
||||
if (method === "GET") {
|
||||
for (var vk in vals) {
|
||||
url += (url.indexOf("?") >= 0 ? "&" : "?") + encodeURIComponent(vk) + "=" + encodeURIComponent(vals[vk]);
|
||||
|
||||
@@ -206,7 +206,7 @@ def _render(expr: Any, env: dict[str, Any]) -> str:
|
||||
return ""
|
||||
return _render_list(expr, env)
|
||||
|
||||
# --- dict → skip (data, not renderable) -------------------------------
|
||||
# --- dict → skip (data, not renderable as HTML content) -----------------
|
||||
if isinstance(expr, dict):
|
||||
return ""
|
||||
|
||||
@@ -540,6 +540,9 @@ def _render_element(tag: str, args: list, env: dict[str, Any]) -> str:
|
||||
parts.append(f" {attr_name}")
|
||||
elif attr_val is True:
|
||||
parts.append(f" {attr_name}")
|
||||
elif isinstance(attr_val, dict):
|
||||
from .parser import serialize as _sx_serialize
|
||||
parts.append(f' {attr_name}="{escape_attr(_sx_serialize(attr_val))}"')
|
||||
else:
|
||||
parts.append(f' {attr_name}="{escape_attr(str(attr_val))}"')
|
||||
parts.append(">")
|
||||
|
||||
@@ -30,8 +30,8 @@ from typing import Any
|
||||
|
||||
from .jinja_bridge import sx
|
||||
|
||||
SEARCH_HEADERS_MOBILE = '{"X-Origin":"search-mobile","X-Search":"true"}'
|
||||
SEARCH_HEADERS_DESKTOP = '{"X-Origin":"search-desktop","X-Search":"true"}'
|
||||
SEARCH_HEADERS_MOBILE = {"X-Origin": "search-mobile", "X-Search": "true"}
|
||||
SEARCH_HEADERS_DESKTOP = {"X-Origin": "search-desktop", "X-Search": "true"}
|
||||
|
||||
|
||||
def render_page(source: str, **kwargs: Any) -> str:
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
:description "sx-vals adds extra key/value pairs to the request parameters. sx-headers adds custom HTTP headers. The server echoes back what it received."
|
||||
: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 \"/examples/api/echo-vals\"\n :sx-vals \"{\\\"source\\\": \\\"button\\\"}\"\n \"Send with vals\")\n\n;; Send custom headers\n(button\n :sx-get \"/examples/api/echo-headers\"\n :sx-headers \"{\\\"X-Custom-Token\\\": \\\"abc123\\\"}\"\n \"Send with headers\")"
|
||||
:sx-code ";; Send extra values with the request\n(button\n :sx-get \"/examples/api/echo-vals\"\n :sx-vals \"{\\\"source\\\": \\\"button\\\"}\"\n \"Send with vals\")\n\n;; Send custom headers\n(button\n :sx-get \"/examples/api/echo-headers\"\n :sx-headers {:X-Custom-Token \"abc123\"}\n \"Send with headers\")"
|
||||
:handler-code "@bp.get(\"/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(\"/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 (...))')"
|
||||
:comp-placeholder-id "vals-comp"
|
||||
:wire-placeholder-id "vals-wire"))
|
||||
|
||||
@@ -705,7 +705,7 @@
|
||||
:sx-get "/examples/api/echo-headers"
|
||||
:sx-target "#headers-result"
|
||||
:sx-swap "innerHTML"
|
||||
:sx-headers "{\"X-Custom-Token\": \"abc123\", \"X-Request-Source\": \"demo\"}"
|
||||
:sx-headers {:X-Custom-Token "abc123" :X-Request-Source "demo"}
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded hover:bg-violet-700 transition-colors text-sm"
|
||||
"Send with headers")
|
||||
(div :id "headers-result" :class "p-3 rounded bg-stone-50 text-sm text-stone-400"
|
||||
|
||||
@@ -272,7 +272,7 @@
|
||||
(defcomp ~ref-headers-demo ()
|
||||
(div :class "space-y-3"
|
||||
(button :sx-get "/reference/api/echo-headers"
|
||||
:sx-headers "{\"X-Custom-Token\": \"abc123\", \"X-Request-Source\": \"demo\"}"
|
||||
:sx-headers {:X-Custom-Token "abc123" :X-Request-Source "demo"}
|
||||
:sx-target "#ref-headers-result"
|
||||
:sx-swap "innerHTML"
|
||||
:class "px-4 py-2 bg-violet-600 text-white rounded text-sm hover:bg-violet-700"
|
||||
|
||||
Reference in New Issue
Block a user