re-frame

jlfischer 2026-02-04T22:20:48.640869Z

Hey, just curious, but how do people go about handling things like context menus in a re-frame application? I'm not sure how I want to catch clicks outside the component to dismiss it.

p-himik 2026-02-04T22:45:44.434419Z

Not really related to re-frame itself. Two common approaches: 1. A backdrop element that covers the whole screen that, when clicked, sends some kind of signal that the associated menu should be closed 2. A global click listener that checks whether there is some menu that it closes upon firing

jlfischer 2026-02-04T22:49:50.534209Z

I was wondering more along the lines of how people choose to store that bit of info in their app-db.

jlfischer 2026-02-04T22:52:32.149429Z

Like, do you have a set of popup toggle states in your app-db so that a generic handler can clear them easily, or per-popup ones so you need to know where to find them, etc.

p-himik 2026-02-04T22:54:30.466829Z

If you go the first route, the backdrop is controlled by the context menu. So whatever state in app-db drives the context menu would inextricably also drive the backdrop. If you go the second route - yeah, there must be some coordination, some common knowledge. So the list of all open pop-ups has to be stored in a central location. Could be in app-db if that list would affect something else, could be a separate atom. I myself prefer going the first route anyway. No state to track.

👍 1
wevrem 2026-02-05T03:54:19.483029Z

Here's how I do it. I use re-frame and reagent in my app, but for this I don't use re-frame at all. Also for this I don't use a backdrop element (I do use a full screen backdrop element when displaying a modal). I use reagent and I toggle visibility using a local reagent atom showing?. The CSS classes used here (.dropdown, .dropdown-trigger, etc.) don't really play a role in the functionality, they just define things like position: relative and z-index.

(ns elements.dropdown
  (:require [reagent.core :as r]))

(defn escape? [e]
  (let [e (or e (.event js/window))]
    (= "Escape" (.-key e))))

(defn element 
  "Provide a 'trigger' that takes a fn 'toggle' to hide/show the dropdown.
   Also provide 'content' that takes a fn 'close'."
  [trigger content]
  (r/with-let [showing? (r/atom false)
               toggle (fn [] (swap! showing? not))
               close (fn [_e] (reset! showing? false))
               close-escape (fn [e] (when (escape? e) (close e)))
               _ (.addEventListener js/document "keydown" close-escape)
               _ (.addEventListener js/document "click" close)]
    [:div.dropdown
     {:on-click #(.stopPropagation %)}   ;; prevent a click from reaching global listener
     [:div.dropdown-trigger
      [trigger toggle]]
     (when @showing?
       [:div.dropdown-content
        [content close]])]
    (finally
      (.removeEventListener js/document "keydown" close-escape)
      (.removeEventListener js/document "click" close))))
Then where I want to use the dropdown...
(ns app
  (:require
   [elements.dropdown :as dropdown]))

(defn trigger [toggle]
  ... somewhere in here call `toggle` to hide/show ...
  )

(defn content [close]
  ... can call `close` if needed ...
  )

(defn app []
  ...
  [dropdown/element trigger content]
  ...
  )

❤️ 1
vanelsas 2026-02-05T09:13:07.653279Z

I use a hook that detects clicks outside, inside and handles pressing escape as well. The hook is attached as a :ref to a div. And the div is basically a div that wraps around the component you want to show this behaviour. Works for modals, side panels , context menus etc.

(ns app.components.generic.hooks.use-click-away
  (:require
    ["react" :refer [useEffect useRef]]))


(defn use-click-away
  "Hook-style utility. Returns a `ref` to attach to an element. Dispatches `close-event` on:
      - outside click,
      - Escape key,
      - internal click on element with `data-close-on-click`."
  [close-fn]
  (let [ref (useRef nil)]
    (useEffect
      (fn []
        (let [handle-click-away (fn [e]
                                  (when (and (.-current ref)
                                             (not (.contains (.-current ref) (.-target e)))
                                             (not (.closest (.-target e) ".modal")))
                                    (.stopPropagation e)
                                    (close-fn)))

              handle-escape (fn [e]
                              (when (= "Escape" (.-key e))
                                (.stopPropagation e)
                                (close-fn)))

              handle-inner-click (fn [e]
                                   (when (.hasAttribute (.-target e) "data-close-on-click")
                                     (.stopPropagation e)
                                     (close-fn)))]

          ;; Attach listeners
          (.addEventListener js/document "mousedown" handle-click-away)
          (.addEventListener js/document "keydown" handle-escape)
          (when (.-current ref)
            (.addEventListener (.-current ref) "click" handle-inner-click))

          ;; Cleanup
          (fn []
            (.removeEventListener js/document "mousedown" handle-click-away)
            (.removeEventListener js/document "keydown" handle-escape)
            (when (.-current ref)
              (.removeEventListener (.-current ref) "click" handle-inner-click)))))
      #js [(.-current ref)]) ; Only re-run effect if ref changes

    ;; Return ref to attach to DOM element
    ref))

❤️ 1