Skip to content

Instantly share code, notes, and snippets.

@borkdude
Created October 27, 2025 07:56
Show Gist options
  • Select an option

  • Save borkdude/ff65e3c5a10cb657e7df68aec35defc6 to your computer and use it in GitHub Desktop.

Select an option

Save borkdude/ff65e3c5a10cb657e7df68aec35defc6 to your computer and use it in GitHub Desktop.

Revisions

  1. borkdude created this gist Oct 27, 2025.
    207 changes: 207 additions & 0 deletions ohm.cljs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,207 @@
    (ns reagami.core
    {:clj-kondo/config '{:linters {:unresolved-symbol {:exclude [update!]}}}})

    (def svg-ns "http://www.w3.org/2000/svg")

    (defn- parse-tag
    "From hiccup, thanks @weavejester"
    [^String tag]
    (let [id-index (let [index (.indexOf tag "#")] (when (pos? index) index))
    class-index (let [index (.indexOf tag ".")] (when (pos? index) index))]
    [(cond
    id-index (.substring tag 0 id-index)
    class-index (.substring tag 0 class-index)
    :else tag)
    (when id-index
    (if class-index
    (.substring tag (inc id-index) class-index)
    (.substring tag (inc id-index))))
    (when class-index
    (.substring tag (inc class-index)))]))

    (def properties (js/Set. [#_"value" "checked" "disabled" "selected"]))

    (defn property? [^js x]
    (.has properties x))

    (do
    #?@(:squint []
    :cljs [(defn keyword->str [k]
    (if (keyword? k)
    (name k)
    k))]))

    #?(:squint (defn array-seq [s]
    s))

    #?(:squint (defn name [s]
    s))

    (defn camel->kebab [s]
    (.replace s (js/RegExp. "[A-Z]" "g")
    (fn [m]
    (str "-" (.toLowerCase m)))))

    (defn- create-node*
    [hiccup in-svg?]
    (cond
    (or (nil? hiccup)
    (string? hiccup)
    (number? hiccup)
    (boolean? hiccup))
    (js/document.createTextNode (str hiccup))
    (vector? hiccup)
    (let [[tag & children] hiccup
    #?@(:squint []
    :cljs [tag (if (keyword? tag)
    (subs (str tag) 1)
    tag)])
    [tag id class] (if (string? tag) (parse-tag tag) [tag])
    classes (when class (.split class "."))
    [attrs children] (if (map? (first children))
    [(first children) (rest children)]
    [nil children])
    in-svg? (or in-svg? (= :svg tag))
    node (if (fn? tag)
    (let [res (apply tag (if attrs
    (cons attrs children)
    children))]
    (create-node* res in-svg?))
    (let [node (if in-svg?
    (js/document.createElementNS svg-ns tag)
    (js/document.createElement tag))]
    (doseq [child children]
    (let [child-nodes (if (and (seq? child)
    (not (vector? child)))
    (mapv #(create-node* % in-svg?) child)
    [(create-node* child in-svg?)])]
    (doseq [child-node child-nodes]
    (.appendChild node child-node))))
    (let [modified-attrs (js/Set.)]
    (doseq [[k v] attrs]
    (let [k (name k)
    #?@(:squint [] :cljs [v (keyword->str v)])]
    (if (.startsWith k "on")
    (let [event (-> k
    (.replaceAll "-" "")
    (.toLowerCase))]
    (aset node event v)
    (.add modified-attrs event))
    (do
    (.add modified-attrs k)
    (cond
    (and (= "style" k) (map? v))
    (do (let [style (reduce-kv
    (fn [s k v]
    (str s (camel->kebab (name k)) ": " (name v) ";"))
    "" v)]
    ;; set/get attribute is faster to set, get
    ;; and compare (in patch)than setting
    ;; individual props and using cssText
    (.setAttribute node "style" style)))
    (property? k) (aset node k v)
    :else (when v (.setAttribute node k v)))))))
    (when (seq classes)
    (.setAttribute node "class"
    (str (when-let [c (.getAttribute node "class")]
    (str c " "))
    (.join classes " ")))
    (.add modified-attrs "class"))
    (when id
    (.setAttribute node "id" id)
    (.add modified-attrs "id"))
    (aset node ::attrs modified-attrs))
    node))]
    node)
    :else
    (throw (do
    (js/console.error "Invalid hiccup:" hiccup)
    (js/Error. (str "Invalid hiccup: " hiccup))))))

    (defn- create-node [hiccup]
    (create-node* hiccup false))

    (defn- patch [^js parent new-children]
    (let [old-children (.-childNodes parent)]
    (if (not= (alength old-children) (alength new-children))
    (.apply parent.replaceChildren parent new-children)
    (doseq [[^js old ^js new] (mapv vector (array-seq old-children) (array-seq new-children))]
    (cond
    (and old new (= (.-nodeName old) (.-nodeName new)))
    (if (= 3 (.-nodeType old))
    (let [txt (.-textContent new)]
    (set! (.-textContent old) txt))
    (do
    (let [^js old-attrs (aget old ::attrs)
    ^js new-attrs (aget new ::attrs)]
    (doseq [o old-attrs]
    (when-not (.has new-attrs o)
    (if (or (.startsWith o "on") (property? o))
    (aset old o nil)
    (.removeAttribute old o))))
    (doseq [n new-attrs]
    (if (or (.startsWith n "on")
    (property? n))
    (let [new-prop (aget new n)]
    (when-not (identical? (aget old n)
    new-prop)
    (aset old n new-prop)))
    (let [new-attr (.getAttribute new n)]
    (when-not false #_(identical? new-attr (.getAttribute old n))
    (.setAttribute old n (.getAttribute new n)))))))
    (when-let [new-children (.-childNodes new)]
    (patch old new-children))))
    :else (.replaceChild parent new old))))))

    (defn reagami:render [root hiccup]
    (let [new-node (create-node hiccup)]
    (patch root #js [new-node])))


    (defn slider [the-atom calc-fn param value min max step invalidates]
    [:input {:type "range"
    :value value
    :min min
    :max max
    :step step
    :style {:width "50%"}
    :on-input (fn [e]
    (let [new-value (parse-double
    (aget (aget e "target") "value"))]

    (swap! the-atom
    (fn [data]
    (-> data
    (assoc param new-value)
    (dissoc invalidates)
    calc-fn)))))}])

    (defn calc-ohms [{:keys [voltage current resistance] :as data}]
    (if (nil? voltage)
    (assoc data :voltage (* current resistance))
    (assoc data :current (/ voltage resistance))))

    (def ohms-data (atom {:voltage 12 :current 0.5 :resistance 24}))

    (defn ohms-law-page []
    (let [{:keys [voltage current resistance]} @ohms-data]
    [:div
    [:h3 "Ohm's Law Calculator"]
    [:div
    "Voltage: " (.toFixed voltage 2) "V"
    [slider ohms-data calc-ohms :voltage voltage 0 30 0.1 :current]]
    [:div
    "Current: " (.toFixed current 2) "A"
    [slider ohms-data calc-ohms :current current 0 3 0.01 :voltage]]
    [:div
    "Resistance: " (.toFixed resistance 2) ""
    [slider ohms-data calc-ohms :resistance resistance 0 100 1 :voltage]]]))

    (defn render []
    (reagami:render (js/document.querySelector "#app")
    [ohms-law-page]))

    (add-watch ohms-data :state (fn [_ _ _ _]
    (render)))

    (render)