Vrac + Signaali + Si-Frame Code Generation Guide

October 13, 2025 ยท View on GitHub

Dependencies

taipei.404.vrac/vrac {:mvn/version "0.1.2"}

Namespace Setup

(ns example.app
  (:require [vrac.web :as vw :refer [$]]
            [signaali.reactive :as sr]
            [re-frame.core :as rf]))  ; Optional, for Si-Frame integration

Element Creation with $

  • HTML elements: ($ :div ...) ($ :span#id.class1.class2 ...)
  • Fragments: ($ :<> child1 child2 ...)
  • Components: ($ component-fn arg1 arg2)
  • Children: strings, numbers, booleans, keywords, vectors, nil (renders nothing), reactive nodes

Properties (Props)

Static Props

($ :div.class1 {:style {:color "red" :padding "1em"}
                :class [:class2]
                :data-testid "my-div"}
   "content")

Reactive Props

Use vw/props-effect with a function returning a prop map:

($ :input
   (vw/props-effect (fn [] {:value @text-signal}))
   {:type "text"
    :on/input (fn [e] (reset! text-signal (.. e -target -value)))})

Prop Namespaces

  • :a/... - HTML attributes (e.g., :a/for, :a/readonly)
  • :p/... - DOM properties (e.g., :p/htmlFor, :p/innerHTML)
  • :on/... - Event handlers (e.g., :on/click, :on/input)
  • No namespace - Vrac special props (:style, :class, :ref, :data-*) or DOM properties

Multiple Prop Maps

Props compose together; later values override earlier ones (except :style and :class which merge):

($ :div {:class :base-class}
        {:class :extra-class}  ; Results in both classes
        {:style {:color "red"}}
        {:style {:padding "1em"}})  ; Both styles applied

Reactive State (Signaali)

Creating State

(let [counter (sr/create-state 0)           ; Mutable state
      text (sr/create-signal "hello")       ; Immutable signal
      doubled (sr/create-derived (fn [] (* 2 @counter)))  ; Computed
      memoized (sr/create-memo (fn [] (expensive-calc @counter)))]  ; Cached computed
  ...)

Using State

  • Read: @signal-or-state
  • Write: (reset! signal-or-state new-value) or (swap! signal-or-state update-fn)
  • Reactive nodes auto-update UI when dereferenced in render

Reactive Fragments

Don't use reactive fragment on static data

Conditional Rendering

;; If/else
(vw/if-fragment condition-fn
  ($ :div "then branch")
  ($ :div "else branch"))

;; When (no else)
(vw/when-fragment condition-fn
  ($ :div "shown when true"))

;; Case
(vw/case-fragment value-fn
  :route/home ($ :div "Home")
  :route/blog ($ :div "Blog")
  ($ :div "Default"))

;; Cond
(vw/cond-fragment
  (= @route :home) ($ :div "Home")
  (= @route :blog) ($ :div "Blog")
  :else ($ :div "Default"))

Important: Pass a function to vw/if-fragment, vw/when-fragment, and vw/case-fragment:

;; Correct:
(vw/if-fragment (fn [] (even? @counter)) ...)
(vw/if-fragment counter ...)  ; OK if counter is already a reactive node

;; Wrong:
(vw/if-fragment (even? @counter) ...)  ; Don't evaluate directly

List Rendering

(vw/for-fragment*
  coll-fn          ; (fn [] @items) or just items-signal
  key-fn           ; :id or (fn [item] (:id item)), optional
  item-component)  ; (fn [item] ($ :div (:name item)))

Components

Define as functions returning vcup:

(defn person-card [person]
  ($ :div.card
     ($ :h3 (:name person))
     ($ :p (:bio person))))

;; Use with $
($ person-card {:name "Alice" :bio "Developer"})

Context

;; Set context
(vw/with-context (sr/create-signal {:theme "dark"})
  ($ child-component))

;; Update based on parent context
(vw/with-context-update (fn [parent-ctx]
                          (assoc @parent-ctx :nested true))
  ($ child-component))

;; Read context
(defn child-component []
  (let [ctx (vw/get-context)]
    ($ :div "Theme: " (sr/create-derived (fn [] (:theme @ctx))))))

Refs

Capture DOM elements:

(let [element-ref (sr/create-signal nil)]
  ($ :div
     ($ :input {:ref element-ref})
     ($ :button {:on/click (fn [] (.focus @element-ref))} "Focus input")))

Re-frame Integration

;; Subscriptions
(rf/reg-sub :counter (fn [db _] (:counter db)))

;; In components - wrap subscriptions for derived values
(defn counter-display []
  (let [counter (rf/subscribe [:counter])
        doubled (sr/create-derived (fn [] (* 2 @counter)))]
    ($ :div "Counter: " counter " Doubled: " doubled)))

;; Dispatch events
($ :button {:on/click #(rf/dispatch [:increment])} "+")

Application Entry Point

(defn mount-ui []
  (vw/render (js/document.getElementById "app")
             ($ root-component)))

(defn ^:dev/after-load reload! []
  (mount-ui))

(defn ^:dev/before-load shutdown! []
  (vw/dispose-render-effects))

(defn init []
  (mount-ui))

SVG and MathML

Use standard element names:

;; SVG
($ :svg {:width "100" :height "100" :xmlns "http://www.w3.org/2000/svg"}
   ($ :circle {:cx 50 :cy 50 :r 40 :fill "blue"}))

;; MathML
($ :math {:display "block"}
   ($ :mrow
      ($ :mi "x")
      ($ :mo "+")
      ($ :mn "1")))

;; Convert HTML/SVG string to DOM
(vw/html-text-to-dom "<svg>...</svg>")

Common Patterns

Counter

(defn counter []
  (let [count (sr/create-state 0)]
    ($ :div
       "Count: " count
       ($ :button {:on/click #(swap! count inc)} "+"))))

Controlled Input

(defn text-input []
  (let [value (sr/create-signal "")]
    ($ :input
       (vw/props-effect (fn [] {:value @value}))
       {:on/input (fn [e] (reset! value (.. e -target -value)))})))

Toggle Visibility

(defn collapsible [title content]
  (let [open? (sr/create-state false)]
    ($ :div
       ($ :button {:on/click #(swap! open? not)} title)
       (vw/when-fragment open?
         ($ :div content)))))