Skip to content

Instantly share code, notes, and snippets.

@AxelRHD
Last active April 9, 2026 21:15
Show Gist options
  • Select an option

  • Save AxelRHD/4d2db6464076f4e89b05cf043f488b7a to your computer and use it in GitHub Desktop.

Select an option

Save AxelRHD/4d2db6464076f4e89b05cf043f488b7a to your computer and use it in GitHub Desktop.
Vial Web Framework (Janet)
[:script (raw "on(document, 'mymsg', ev => alert(ev.detail.message))")]
(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)))
(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