Last active
April 9, 2026 21:15
-
-
Save AxelRHD/4d2db6464076f4e89b05cf043f488b7a to your computer and use it in GitHub Desktop.
Vial Web Framework (Janet)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [:script (raw "on(document, 'mymsg', ev => alert(ev.detail.message))")] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (use vial) | |
| (import json) | |
| (import ./trigger) | |
| # Layout | |
| (defn app-layout [{:body body :request request}] | |
| (text/html | |
| (doctype :html5) | |
| [:html {:lang "en"} | |
| [:head | |
| [:title "vtest"] | |
| [:meta {:charset "utf-8"}] | |
| [:meta {:name "viewport" :content "width=device-width, initial-scale=1"}] | |
| [:meta {:name "csrf-token" :content (csrf-token-value request)}] | |
| [:link {:rel "icon" :href "/favicon.ico"}] | |
| [:link {:href "/app.css" :rel "stylesheet"}] | |
| # [:script {:src "/app.js" :defer ""}] | |
| [:script {:src "https://cdn.jsdelivr.net/gh/gnat/surreal@main/surreal.js"}]] | |
| [:body | |
| [:script (raw "on(document, 'mymsg', ev => alert(ev.detail.message))")] | |
| body]])) | |
| # Routes | |
| (def router (new-router)) | |
| (defn home [request] | |
| # Example: push a client trigger via the Vial-Trigger response header. | |
| # The browser can read it and react however you want (toast, alert, ...). | |
| # (triggers/add request {:type "toast" :message "Welcome!" :level "info"}) | |
| [:div {:class "tc"} | |
| [:img {:src "/vial_logo_transparent.png" :alt "vial" :width "150"}] | |
| [:h1 "Freshly brewed with vial."] | |
| [:p {:class "code"} | |
| [:b "Vial Version:"] | |
| [:span (string " " version)]] | |
| [:p {:class "code"} | |
| [:b "Janet Version:"] | |
| [:span janet/version]]]) | |
| (GET router "/" home) | |
| (defn ping [request] | |
| (application/json {:ping "pong"})) | |
| (GET router "/ping" ping) | |
| (defn trigger-test [request] | |
| [:div | |
| [:h1 | |
| "Hello Trigger"] | |
| (trigger/add request {:type "mymsg" :message "Hello from Trigger-Element."})]) | |
| (GET router "/trigger-test" trigger-test) | |
| # Middleware + Server | |
| (def app (-> (compile-router router) | |
| (trigger/middleware) | |
| (layout app-layout) | |
| (with-csrf-token) | |
| (with-session) | |
| (defaults))) | |
| # For fine-grained control, replace (defaults) with individual middleware: | |
| # (def app (-> (compile-router router) | |
| # (triggers/middleware) # collects triggers, writes Vial-Trigger header | |
| # (layout app-layout) # wrap hiccup responses in layout | |
| # (with-csrf-token) # CSRF protection (needs with-session) | |
| # (with-session) # encrypted cookie sessions | |
| # (extra-methods) # (default) support _method field for PUT/PATCH/DELETE | |
| # (query-string) # (default) parse query string params | |
| # (body-parser) # (default) parse form-encoded bodies | |
| # (json-body-parser) # (default) parse JSON bodies | |
| # (server-error) # (default) catch errors, show debug page in dev | |
| # (x-headers) # (default) security headers (X-Frame-Options etc.) | |
| # (static-files) # (default) serve files from ./public | |
| # (not-found) # (default) 404 fallback | |
| # (logger))) # (default) request logging | |
| # Server | |
| (defn main [& args] | |
| (let [port (get args 1 (or (env :port) "8080")) | |
| host (get args 2 "localhost")] | |
| (print (string "vial running on " host ":" port "...")) | |
| (server app port host))) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (use vial) | |
| (import json) | |
| (setdyn :doc `Server-to-client event triggers for Vial. Requires surreal.js. | |
| https://github.com/gnat/surreal | |
| Server code pushes events during a request via (trigger/add ...). The | |
| middleware injects a self-removing HTML fragment into the hiccup response | |
| that dispatches each event as a CustomEvent on document. The client side | |
| is just listeners — no header parsing, no polling, no framework lock-in. | |
| ## Setup | |
| Load surreal.js (and any toast/UI library you want to drive) in your layout: | |
| [:script {:src "https://cdn.jsdelivr.net/gh/gnat/surreal@v1.4.0/surreal.js"}] | |
| Add trigger/middleware **innermost** in your stack so it sees raw hiccup | |
| from the handler before the layout middleware wraps it into a dict: | |
| (def app (-> (compile-router router) | |
| (trigger/middleware) # innermost — must wrap the router directly | |
| (layout app-layout) | |
| (with-csrf-token) | |
| (with-session) | |
| (defaults))) | |
| Register listeners anywhere in your layout body: | |
| [:script (raw "on(document, 'toast', ev => myToast(ev.detail))")] | |
| [:script (raw "on(document, 'refresh-table', ev => htmx.trigger('#t','load'))")] | |
| ## Pushing events | |
| From any handler that returns hiccup: | |
| (defn save [request] | |
| ; ... do the work ... | |
| [:div | |
| [:h1 "Saved"] | |
| (trigger/add request {:type "toast" :level "success" :message "Saved!"})]) | |
| trigger/add returns nil, so embedding it inline as a hiccup child renders | |
| nothing while the side-effect queues the event. Multiple add calls | |
| accumulate and all fire in order on the client. | |
| Each event must have a :type key — that becomes the CustomEvent name. | |
| Avoid dots in type names if you use Alpine (it treats them as modifier | |
| separators); hyphens are fine. | |
| ## Debugging | |
| Set this flag in the browser console to keep the injected fragment in the | |
| DOM instead of removing itself after dispatch — useful for inspecting | |
| which events fired and what payload they carried: | |
| window.TRIGGER_DEBUG = true; | |
| The JSON payload remains visible in the element inspector under | |
| <script type="application/json" class="trigger-data">. Reset with | |
| window.TRIGGER_DEBUG = false or a page reload. | |
| ## Surviving a redirect | |
| Dict responses (redirects, application/json, text/html) bypass the | |
| middleware — events queued before such a response are silently dropped. | |
| When you need to push an event *and* redirect, route it through Vial's | |
| flash and add a tiny userland bridge that copies flash → triggers on | |
| the next request: | |
| (defn flash->triggers [handler] | |
| (fn [request] | |
| (each msg (get request :flash []) (trigger/add request msg)) | |
| (handler request))) | |
| (def app (-> (compile-router router) | |
| (flash->triggers) # inside trigger/middleware | |
| (trigger/middleware) | |
| (layout app-layout) | |
| (flash/middleware) # outside, so :flash is on request | |
| (with-csrf-token) | |
| (with-session) | |
| (defaults))) | |
| Then the redirect-flow becomes: | |
| (defn create [request] | |
| (flash/put request {:type "toast" :level "success" :message "Saved!"}) | |
| (redirect-to request :get-home)) | |
| Caveat: if a handler that consumed flash *also* returns a dict (e.g. yet | |
| another redirect with no body), the flash-derived events are lost — the | |
| middleware can only inject into hiccup. Re-flash explicitly in those cases. | |
| `) | |
| (defn- json-script-safe | |
| ``Encode events as JSON safe to embed inside a <script> tag. Escapes </ | |
| to <\/ so a stray </script> in payload data can't break out of the script | |
| element.`` | |
| [events] | |
| (->> (json/encode events) | |
| (string/replace-all "</" "<\\/"))) | |
| (defn element | |
| ``Render a self-removing HTML fragment that dispatches each event in `events` | |
| as a CustomEvent on document. Each event must have a :type key. Returns nil | |
| for empty input — safe to call unconditionally. | |
| Useful when you want to render triggers manually outside the normal | |
| middleware flow (e.g. embedding directly into a hiccup template).`` | |
| [events] | |
| (when (and events (not (empty? events))) | |
| [:div | |
| [:script {:type "application/json" :class "trigger-data"} | |
| (raw (json-script-safe events))] | |
| [:script (raw ` | |
| const data = JSON.parse(me('prev').textContent); | |
| data.forEach(msg => send(document, msg.type, msg)); | |
| if (!window.TRIGGER_DEBUG) me().remove(); | |
| `)]])) | |
| (defn add | |
| ``Push an event onto the request's trigger queue. Returns nil so it can | |
| be embedded inline as a hiccup child without rendering anything. | |
| Requires trigger/middleware to be in the stack. | |
| Example: | |
| (defn handler [request] | |
| [:div | |
| [:h1 "Hello"] | |
| (trigger/add request {:type "toast" :message "Saved!"})])`` | |
| [request event] | |
| (unless (indexed? (get request :triggers)) | |
| (put request :triggers @[])) | |
| (array/push (get request :triggers) event) | |
| nil) | |
| (defn middleware | |
| ``Collects events added via trigger/add and appends a self-dispatching | |
| fragment to hiccup responses. Must run **innermost** in the -> stack so | |
| it wraps the router directly and sees raw hiccup before layout converts | |
| it to a dict response. | |
| Dict and string responses pass through unchanged — for events that need | |
| to survive a redirect, use flash + a flash->triggers bridge (see module | |
| docstring).`` | |
| [handler] | |
| (fn [request] | |
| (unless (indexed? (get request :triggers)) | |
| (put request :triggers @[])) | |
| (let [response (handler request) | |
| events (get request :triggers @[])] | |
| (cond | |
| (empty? events) response | |
| (indexed? response) [;response (element events)] | |
| response)))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment