- Fix highlight() returning SxExpr so syntax-highlighted code renders as DOM elements instead of leaking SX source text into the page - Add Specs section that reads and displays canonical SX spec files from shared/sx/ref/ with syntax highlighting - Add "The Reflexive Web" essay on SX becoming a complete LISP with AI as native participant - Change logo from (<x>) to (<sx>) everywhere - Unify all backgrounds to bg-stone-100, center code blocks - Skip component/style cookie cache in dev mode so .sx edits are visible immediately on refresh without clearing localStorage Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
368 lines
13 KiB
Plaintext
368 lines
13 KiB
Plaintext
;; 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 "/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."))))
|