Search code examples
clojurescriptreagent

Tracking mouse and render dot at the screen in clojurescript / reagent?


I am quite new to clojurescript and maybe this is a trivial question but I did not manage to find the an answer yet.

I am looking forward to implementing a clojurescript to track the mouse and render a dot at the mouse position, as implemented here:

https://jsbin.com/gejuz/1/edit?html,output

Js Code:

function() {
  "use strict";

  document.onmousemove = handleMouseMove;
  function handleMouseMove(event) {
  var dot, eventDoc, doc, body, pageX, pageY;

  event = event || window.event; // IE-ism

  // If pageX/Y aren't available and clientX/Y
  // are, calculate pageX/Y - logic taken from jQuery
        // Calculate pageX/Y if missing and clientX/Y available
  if (event.pageX == null && event.clientX != null) {
    eventDoc = (event.target && event.target.ownerDocument) || document;
    doc = eventDoc.documentElement;
    body = eventDoc.body;

    event.pageX = event.clientX +
      (doc && doc.scrollLeft || body && body.scrollLeft || 0) -
      (doc && doc.clientLeft || body && body.clientLeft || 0);
    event.pageY = event.clientY +
      (doc && doc.scrollTop  || body && body.scrollTop  || 0) -
      (doc && doc.clientTop  || body && body.clientTop  || 0 );
  }

  // Add a dot to follow the cursor
  dot = document.createElement('div');
  dot.className = "dot";
  dot.style.left = event.pageX + "px";
  dot.style.top = event.pageY + "px";
  document.body.appendChild(dot);
}

Up until now, I did manage to get the mouse coordinates (thanks to this question Tracking mouse in clojurescript / reagent / reagi?). But I am failing to render the dot in the webpage.

Clojurescript code:

(def mouse-coordinates (reagent/atom {:x 100 :y 100}))

(defn dot [x y]
  [:div {:style {:left (str x "px")
                 :top (str y "px")
                 :width "2px"
                 :height "2px"
                 :background-clor "black"
                 :position "absolute"}}])

(def do-dot (reagent/reactify-component dot))

(defn mouse-move []
  [:body
   {:onMouseMove (fn [event]
                   (swap! mouse-coordinates assoc :x (.-clientX event))
                   (swap! mouse-coordinates assoc :y (.-clientY event))
                   (reagent/create-element do-dot
                                           #js{:x (int (:x @mouse-coordinates))
                                               :y (int (:y @mouse-coordinates))})
                   )}
   [:p "x: " (int (:x @mouse-coordinates))]
   [:p "y: " (int (:y @mouse-coordinates))]
   ])


(reagent/render-component [mouse-move]
                          (. js/document (getElementById "app")))

Any help is appreciated. Thank you in advance.


Solution

  • Instead of creating an element in the onMouseMove event, you can include your dot component as part of the rendering code. It will pick up changes to the reagent/atom just like the two p elements are doing:

       [:p "x: " (int (:x @mouse-coordinates))]
       [:p "y: " (int (:y @mouse-coordinates))]
       [dot (int (:x @mouse-coordinates)) (int (:y @mouse-coordinates))]
    

    There's also a typo: :background-clor -> :background-color. These two changes should be enough to make the dot show up.


    And a couple of other things to help simplify the code:

    style properties will default to pixels if you pass in a number

    So the dot component can be written like this:

    (defn dot [x y]
      [:div {:style {:left             x
                     :top              y
                     :width            2
                     :height           2
                     :background-color "black"
                     :position         "absolute"}}])
    

    reset! vs swap!

    • Because mouse-coordinates serves a very specific purpose, it's a bit neater to use reset! instead of swap! in the onMouseMove event:
    (reset! mouse-coordinates {:x (.-clientX event) :y (.-clientY event)})
    

    Pass in component props as a map

    (defn dot [{:keys [x y]}]
      ...)
    
    [dot @mouse-coordinates]
    

    The final code ends up looking like this:

    (def mouse-coordinates (reagent/atom {:x 100 :y 100}))
    
    (defn dot [{:keys [x y]}]
      [:div {:style {:left             x
                     :top              y
                     :width            2
                     :height           2
                     :background-color "black"
                     :position         "absolute"}}])
    
    (defn mouse-move []
      [:body
       {:onMouseMove (fn [event]
                       (reset! mouse-coordinates {:x (.-clientX event) :y (.-clientY event)}))}
       [:p "x: " (:x @mouse-coordinates)]
       [:p "y: " (:y @mouse-coordinates)]
       [dot @mouse-coordinates]])
    

    UPDATE: When I first answered the question I didn't realise each dot should be persistent. Here's the updated code (with comments) on how to achieve this:

    Using a collection of coordinates

    (def mouse-coordinates (r/atom []))
    
    (defn dot [{:keys [x y]}]
      [:div {:style {:left             x
                     :top              y
                     :width            2
                     :height           2
                     :background-color "black"
                     :position         "absolute"}}])
    
    (defn mouse-move []
      [:div
       {:onMouseMove (fn [event]
                       (let [x (.-clientX event)
                             y (.-clientY event)
                             ;; If there's already a dot in an identical location, don't add it. This saves unnecessary work and
                             ;; means we can use [x y] as our unique key for our collection.
                             coords-already-exist? (not (empty? (filter #(and (= (:x %) x) (= (:y %) y)) @mouse-coordinates)))]
                         (when-not coords-already-exist?
                           ;; conj the new coordinate to the collection.
                           (swap! mouse-coordinates #(conj % {:x (.-clientX event) :y (.-clientY event)})))))}
       [:p "x: " (:x @mouse-coordinates)]
       [:p "y: " (:y @mouse-coordinates)]
       ;; Loop through the coordinates.
       (for [{:keys [x y]} @mouse-coordinates]
         [dot
          ;; Important: we give each dot a unique key.
          {:key [x y]
           :x   x
           :y   y}])])
    

    As mentioned in the comments, the important thing about rendering a collection is giving each item a unique key. This means that as new coordinates are created, React knows to append a new child instead of re-rendering every single dot. More info can be found in the React docs on this: https://reactjs.org/docs/lists-and-keys.html#keys