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.
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 numberSo 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!
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)})
(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:
(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