1

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.

b3rt0
  • 769
  • 2
  • 6
  • 21

1 Answers1

3

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

Not so Veteran
  • 318
  • 3
  • 9
  • Thank you! I am however still facing issues with the .appendChild; How to keep all the dots in the page? – b3rt0 Aug 27 '19 at 11:36
  • If you only want a single dot, the code above should be enough and you won't need to use .appendChild anywhere. If you wanted multiple dots to appear at once, I'd still avoid using .appendChild and instead have a collection of multiple coordinates, and put a for loop in the render code (for each coordinate, render a dot). You might need to open another question if you need more info. – Not so Veteran Aug 28 '19 at 06:46
  • I re-read your question and realised I didn't fully answer it... I'll update my answer with some more details when I get a chance (about 8 hours from now). My comment above may still be of use in the meantime – Not so Veteran Aug 28 '19 at 13:01
  • 1
    I like the idea of avoiding .appendChild. I will consider adding a collection of coordinates. I just wonder if that will impact performance in any way. If I find anything I will post results here. Thanks a lot @not-so-veteran – b3rt0 Aug 28 '19 at 20:19
  • I tried the idea of collection of coordinates. It works fine for collecting the data, no issues there. My problem now is how to return all dots? I tried [(map dot @mouse-coordinates)] but it does not work,,, Seems to be something related to the lazy evaluation of lists in clojure. I am not sure about it though. – b3rt0 Aug 28 '19 at 21:31
  • I added my attempt in your answer above. The last line fails - how to return the map of elements? – b3rt0 Aug 28 '19 at 21:40
  • Yes, it works. Thank you for the heads up on the keys. – b3rt0 Aug 29 '19 at 07:41