Skip to content

Instantly share code, notes, and snippets.

@awwx
Created March 27, 2024 03:48
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save awwx/ec8e81c2682b0b67e7c0758dd100b3a9 to your computer and use it in GitHub Desktop.
Save awwx/ec8e81c2682b0b67e7c0758dd100b3a9 to your computer and use it in GitHub Desktop.
A version of Electric's dom/on! which only needs to attach the event listener on mount and remove on unmount
; The goal here is to provide a coupling between Electric
; and JavaScript event listeners that's more idiomatic on the
; JavaScript side: namely, that the event listener can be
; attached only once on mount and only needs to be removed on
; unmount.
;
; The strategy is to use e/fn to convert the reactive event
; listener fn into a continuous flow of event listener functions,
; which we can then sample to get the latest listener function
; when an event occurs.
(ns event-listener
(:require
[missionary.core :as m]
[hyperfiddle.electric :as e]
[hyperfiddle.electric-dom2 :as dom])
#?(:cljs
(:require-macros [event-listener])))
;; Calls `act` on each value of discrete flow `f` for side
;; effects. Returns a flow of nil's for integration into
;; Electric. Is that necessary? I don't know.
(defn mforeach [>f act]
(m/reductions
(fn [_ x] (act x) nil)
nil
>f))
;; A verbose version of e/dom-listener
#?(:cljs (defn verbose-dom-listener [node typ f opts]
(prn "add event listener" (.-id node))
(.addEventListener node typ f (clj->js opts))
(fn []
(prn "remove event listener" (.-id node))
(.removeEventListener node typ f))))
;; Attach an event listener to the node on mount and remove
;; on unmount. Returns a discrete flow of events.
#?(:cljs
(defn mdom-listener> [node event-type opts]
(m/observe
(fn [emit!]
(#_e/dom-listener
verbose-dom-listener node event-type emit! opts)))))
;; Given a discrete flow of events and a continuous flow of
;; listener functions (such as produced by
;; (e/fn (fn [event] ...))), sample the latest listener function
;; at the time an event occurs and call it with the event.
(defn mhandle [>events <listener]
(mforeach
(m/sample vector <listener >events)
(fn [[listener event]]
(listener event))))
#?(:cljs
(defn mon-event [node event-type opts <listener]
(mhandle (mdom-listener> node event-type opts) <listener)))
(e/defn On! [event-type opts listener]
(e/client
(new (mon-event dom/node event-type opts (e/fn [] listener)))))
(defmacro on!
([event-type listener]
`(on! ~event-type {} ~listener))
([event-type opts listener]
`(On!. ~event-type ~opts ~listener)))
(e/defn Main [ring-request]
(e/client
(let [!state (atom :a)
state (e/watch !state)]
(binding [dom/node js/document.body]
;; some buttons to change the state
(dom/div
(dom/text "select: ")
(e/for [id [:a :b :c]]
(dom/button
(dom/props {:id (name id)})
(dom/text (name id))
(dom/on! "click" (fn [_] (reset! !state id))))
(dom/text " ")))
;; display the state for convenience
(dom/pre (dom/text "state: " (pr-str state)))
;; demo: event listener has reactive dependency on state,
;; but the event listener is only attached once.
(dom/div
(dom/button
(dom/props {:id "click-me"})
(dom/text "Click Me")
(on! "click"
(fn [event]
(println
"click"
"shift:" (.-shiftKey event)
"state:" state)))))))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment