Parser: skip unit suffix when next ident is a comparison keyword (starts, ends, contains, matches, is, does, in, precedes, follows). Fixes "123 starts with '12'" returning "123starts" instead of true. eval-hs: use hs-compile directly instead of hs-to-sx-from-source with "return " prefix, which was causing the parser to consume the comparison as a string suffix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
314 lines
12 KiB
Plaintext
314 lines
12 KiB
Plaintext
;; htmx demo — live interactive examples using hx-* attributes
|
|
(defcomp
|
|
()
|
|
(~docs/page
|
|
:title "htmx"
|
|
(p
|
|
(~tw :tokens "text-lg text-gray-600 mb-2")
|
|
"Every "
|
|
(code "hx-*")
|
|
" attribute is syntactic sugar for a "
|
|
(a
|
|
:href "/sx/(applications.(hyperscript))"
|
|
(~tw :tokens "text-violet-600 underline")
|
|
"_hyperscript")
|
|
" event handler. Same runtime, same bytecode, zero duplication.")
|
|
(p
|
|
(~tw :tokens "text-gray-500 mb-8")
|
|
"These demos use real "
|
|
(code "hx-*")
|
|
" attributes processed by "
|
|
(code "htmx-activate!")
|
|
" — not "
|
|
(code "_=\"...\"")
|
|
" hyperscript syntax. "
|
|
"Click buttons, type in inputs, and watch the network tab.")
|
|
(~docs/section
|
|
:title "Click to Load"
|
|
:id "click-to-load"
|
|
(p
|
|
(~tw :tokens "text-gray-600 mb-3")
|
|
"The simplest htmx pattern: a button with "
|
|
(code "hx-get")
|
|
" that loads server content into a target element.")
|
|
(~docs/code
|
|
:src "<button hx-get=\"/sx/(applications.(htmx.(api.click)))\"\n hx-target=\"#click-result\"\n hx-swap=\"innerHTML\">\n Load Content\n</button>")
|
|
(div
|
|
(~tw :tokens "mt-4 flex flex-col gap-3")
|
|
(button
|
|
(~tw
|
|
:tokens "self-start px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
|
|
:hx-get "/sx/(applications.(htmx.(api.click)))"
|
|
:hx-target "#click-result"
|
|
:hx-swap "innerHTML"
|
|
"Load Content")
|
|
(div
|
|
:id "click-result"
|
|
(~tw
|
|
:tokens "min-h-16 border border-dashed border-gray-200 rounded-lg flex items-center justify-center")
|
|
(span
|
|
(~tw :tokens "text-gray-400 text-sm")
|
|
"Content will appear here"))))
|
|
(~docs/section
|
|
:title "Active Search"
|
|
:id "active-search"
|
|
(p
|
|
(~tw :tokens "text-gray-600 mb-3")
|
|
"Search-as-you-type with "
|
|
(code "hx-trigger=\"input changed delay:300ms\"")
|
|
" — debounces input and only fires when the value actually changes.")
|
|
(~docs/code
|
|
:src "<input hx-get=\"/sx/(applications.(htmx.(api.search)))\"\n hx-trigger=\"input changed delay:300ms\"\n hx-target=\"#search-results\"\n name=\"q\"\n placeholder=\"Search patterns...\">")
|
|
(div
|
|
(~tw :tokens "mt-4 space-y-3")
|
|
(input
|
|
(~tw
|
|
:tokens "w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-300 focus:border-violet-400 outline-none")
|
|
:type "text"
|
|
:name "q"
|
|
:placeholder "Search patterns… (try 'tab', 'modal', 'scroll')"
|
|
:hx-get "/sx/(applications.(htmx.(api.search)))"
|
|
:hx-trigger "input changed delay:300ms"
|
|
:hx-target "#search-results")
|
|
(div
|
|
:id "search-results"
|
|
(~tw :tokens "border border-gray-200 rounded-lg overflow-hidden")
|
|
(p (~tw :tokens "text-gray-400 text-sm p-3") "Type to search…"))))
|
|
(~docs/section
|
|
:title "Tabs"
|
|
:id "tabs"
|
|
(p
|
|
(~tw :tokens "text-gray-600 mb-3")
|
|
"Tab buttons with "
|
|
(code "hx-get")
|
|
" loading content from the server. "
|
|
"Each click replaces the tab panel with server-rendered HTML.")
|
|
(~docs/code
|
|
:src "<button hx-get=\"/api/tab?tab=overview\"\n hx-target=\"#tab-content\"\n hx-swap=\"innerHTML\">\n Overview\n</button>")
|
|
(div
|
|
(~tw :tokens "mt-4 border border-gray-200 rounded-lg overflow-hidden")
|
|
(div
|
|
(~tw :tokens "flex border-b border-gray-200 bg-gray-50")
|
|
(button
|
|
(~tw
|
|
:tokens "px-4 py-2 text-sm font-medium text-violet-700 border-b-2 border-violet-600 bg-white")
|
|
:hx-get "/sx/(applications.(htmx.(api.tab)))?tab=overview"
|
|
:hx-target "#htmx-tab-content"
|
|
:hx-swap "innerHTML"
|
|
"Overview")
|
|
(button
|
|
(~tw
|
|
:tokens "px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700")
|
|
:hx-get "/sx/(applications.(htmx.(api.tab)))?tab=features"
|
|
:hx-target "#htmx-tab-content"
|
|
:hx-swap "innerHTML"
|
|
"Features")
|
|
(button
|
|
(~tw
|
|
:tokens "px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700")
|
|
:hx-get "/sx/(applications.(htmx.(api.tab)))?tab=code"
|
|
:hx-target "#htmx-tab-content"
|
|
:hx-swap "innerHTML"
|
|
"Code"))
|
|
(div
|
|
:id "htmx-tab-content"
|
|
(~tw :tokens "p-4")
|
|
(p
|
|
(~tw :tokens "text-gray-400 text-sm")
|
|
"Click a tab to load content"))))
|
|
(~docs/section
|
|
:title "Append Items"
|
|
:id "append"
|
|
(p
|
|
(~tw :tokens "text-gray-600 mb-3")
|
|
"Using "
|
|
(code "hx-swap=\"beforeend\"")
|
|
" to append new items instead of replacing content. "
|
|
"The htmx v4 alias "
|
|
(code "append")
|
|
" also works.")
|
|
(~docs/code
|
|
:src "<button hx-post=\"/api/append\"\n hx-target=\"#item-list\"\n hx-swap=\"beforeend\">\n Add Item\n</button>")
|
|
(div
|
|
(~tw :tokens "mt-4 space-y-3")
|
|
(button
|
|
(~tw
|
|
:tokens "px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
|
|
:hx-post "/sx/(applications.(htmx.(api.append)))"
|
|
:hx-target "#item-list"
|
|
:hx-swap "beforeend"
|
|
"Add Item")
|
|
(div
|
|
:id "item-list"
|
|
(~tw
|
|
:tokens "min-h-16 border border-dashed border-gray-200 rounded-lg p-2")
|
|
(span
|
|
(~tw :tokens "text-gray-400 text-sm")
|
|
"Items will append here"))))
|
|
(~docs/section
|
|
:title "Delete with Confirm"
|
|
:id "delete"
|
|
(p
|
|
(~tw :tokens "text-gray-600 mb-3")
|
|
(code "hx-confirm")
|
|
" shows a native dialog before firing the request. "
|
|
(code "hx-delete")
|
|
" sends a DELETE method. The empty response removes the target.")
|
|
(~docs/code
|
|
:src "<button hx-delete=\"/api/delete\"\n hx-target=\"closest .item\"\n hx-swap=\"outerHTML\"\n hx-confirm=\"Are you sure?\">\n Delete\n</button>")
|
|
(div
|
|
:id "delete-demo"
|
|
(~tw :tokens "mt-4 space-y-2")
|
|
(div
|
|
(~tw
|
|
:tokens "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg item")
|
|
(span "First item")
|
|
(button
|
|
(~tw
|
|
:tokens "px-3 py-1 text-sm text-red-600 border border-red-200 rounded hover:bg-red-50")
|
|
:hx-delete "/sx/(applications.(htmx.(api.delete)))"
|
|
:hx-target "closest .item"
|
|
:hx-swap "outerHTML"
|
|
:hx-confirm "Delete this item?"
|
|
"Delete"))
|
|
(div
|
|
(~tw
|
|
:tokens "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg item")
|
|
(span "Second item")
|
|
(button
|
|
(~tw
|
|
:tokens "px-3 py-1 text-sm text-red-600 border border-red-200 rounded hover:bg-red-50")
|
|
:hx-delete "/sx/(applications.(htmx.(api.delete)))"
|
|
:hx-target "closest .item"
|
|
:hx-swap "outerHTML"
|
|
:hx-confirm "Delete this item?"
|
|
"Delete"))
|
|
(div
|
|
(~tw
|
|
:tokens "flex items-center justify-between p-3 bg-gray-50 border border-gray-200 rounded-lg item")
|
|
(span "Third item")
|
|
(button
|
|
(~tw
|
|
:tokens "px-3 py-1 text-sm text-red-600 border border-red-200 rounded hover:bg-red-50")
|
|
:hx-delete "/sx/(applications.(htmx.(api.delete)))"
|
|
:hx-target "closest .item"
|
|
:hx-swap "outerHTML"
|
|
:hx-confirm "Delete this item?"
|
|
"Delete"))))
|
|
(~docs/section
|
|
:title "Form Submission"
|
|
:id "form"
|
|
(p
|
|
(~tw :tokens "text-gray-600 mb-3")
|
|
"A form with "
|
|
(code "hx-post")
|
|
" sends form data via AJAX. "
|
|
"The response replaces the target — no full page reload.")
|
|
(~docs/code
|
|
:src "<form hx-post=\"/api/form\"\n hx-target=\"#form-result\"\n hx-swap=\"innerHTML\">\n <input name=\"name\" />\n <input name=\"email\" type=\"email\" />\n <button type=\"submit\">Submit</button>\n</form>")
|
|
(div
|
|
(~tw :tokens "mt-4 space-y-3")
|
|
(form
|
|
(~tw :tokens "space-y-3")
|
|
:hx-post "/sx/(applications.(htmx.(api.form)))"
|
|
:hx-target "#form-result"
|
|
:hx-swap "innerHTML"
|
|
(div
|
|
(~tw :tokens "flex gap-3")
|
|
(input
|
|
(~tw
|
|
:tokens "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-300 outline-none")
|
|
:type "text"
|
|
:name "name"
|
|
:placeholder "Name")
|
|
(input
|
|
(~tw
|
|
:tokens "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-violet-300 outline-none")
|
|
:type "email"
|
|
:name "email"
|
|
:placeholder "Email"))
|
|
(button
|
|
(~tw
|
|
:tokens "px-4 py-2 bg-violet-600 text-white rounded-lg hover:bg-violet-700 transition-colors")
|
|
:type "submit"
|
|
"Submit"))
|
|
(div
|
|
:id "form-result"
|
|
(~tw :tokens "min-h-12")
|
|
(span
|
|
(~tw :tokens "text-gray-400 text-sm")
|
|
"Submit the form to see the response"))))
|
|
(~docs/section
|
|
:title "How It Works"
|
|
:id "how-it-works"
|
|
(~docs/note
|
|
(p
|
|
(strong "Zero separate runtime. ")
|
|
"htmx attributes are translated to the same runtime calls as "
|
|
(a
|
|
:href "/sx/(applications.(hyperscript))"
|
|
(~tw :tokens "text-violet-600 underline")
|
|
"_hyperscript")
|
|
" event handlers. "
|
|
(code "hx-get")
|
|
" becomes "
|
|
(code "perform io-fetch")
|
|
". "
|
|
(code "hx-swap=\"innerHTML\"")
|
|
" becomes "
|
|
(code "dom-set-inner-html")
|
|
". "
|
|
"Same bytecode path."))
|
|
(p
|
|
(~tw :tokens "text-gray-600 mt-3")
|
|
"When the page loads, "
|
|
(code "htmx-boot!")
|
|
" scans the DOM for elements with "
|
|
(code "hx-get")
|
|
", "
|
|
(code "hx-post")
|
|
", etc. For each element, "
|
|
(code "htmx-activate!")
|
|
" reads the attributes, builds a handler function "
|
|
"from the same primitives the hyperscript compiler emits, and registers "
|
|
"it via "
|
|
(code "hs-on")
|
|
".")
|
|
(ol
|
|
(~tw :tokens "mt-3 space-y-1 text-gray-600 list-decimal list-inside")
|
|
(li
|
|
(code "htmx-boot!")
|
|
" finds all "
|
|
(code "[hx-get],[hx-post],…")
|
|
" elements")
|
|
(li
|
|
(code "htmx-activate!")
|
|
" reads "
|
|
(code "hx-get")
|
|
", "
|
|
(code "hx-target")
|
|
", "
|
|
(code "hx-swap")
|
|
", "
|
|
(code "hx-trigger")
|
|
" attributes")
|
|
(li
|
|
"Builds a handler: "
|
|
(code
|
|
"(fn (evt) (perform (io-fetch url method)) (hx-swap! target ...))"))
|
|
(li
|
|
"Registers via "
|
|
(code "hs-on")
|
|
" — same event system as hyperscript")
|
|
(li "On trigger, handler fires through the same bytecode VM"))
|
|
(p
|
|
(~tw :tokens "text-gray-600 mt-3")
|
|
"The full implementation is ~400 lines in "
|
|
(code "lib/hyperscript/htmx.sx")
|
|
" with "
|
|
(a
|
|
:href "/sx/(applications.(hyperscript.htmx))"
|
|
(~tw :tokens "text-violet-600 underline")
|
|
"57 tests")
|
|
" covering parsing, swap modes, triggers, and v4 features."))))
|