Search code examples
javascripthtmlweb-componentclojurescriptreagent

ClojureScript Reagent Parent component won't pass cursor to child component


I am trying to create a simple dynamic svg. One where the viewbox settings update with change in the window dimensions.

To do this I have a top level component defined as follows

(defn windowdim_comp
 []
 (with-let [wndcomp_state (atom {:text "Parent2Component"})
         resize_handler #(swap! wndcomp_state assoc
                         :height (.-innerHeight js/window)
                         :width (.-innerWidth js/window))
         mousemove_handler #(swap! wndcomp_state assoc
                                   :x (.-pageX %)
                                   :y (.-pageY %))
         _ (.addEventListener js/window "resize" resize_handler)
         _ (.addEventListener js/document "mousemove" mousemove_handler)
         _ (swap! wndcomp_state assoc :height (.-innerHeight js/window))
         _ (swap! wndcomp_state assoc :width (.-innerWidth js/window))]
[:div
 [:p "width : " (:width @wndcomp_state) " height : " (:height @wndcomp_state)]
 [:p "x : " (:x @wndcomp_state) " y : " (:y @wndcomp_state)]
 (svgrender (cursor wndcomp_state [:text :width :height]))
 ]
(finally
  (.removeEventListener js/window "resize" resize_handler)
  (.removeEventListener js/document "mousemove" mousemove_handler))))

This component then calls a child component called svgrender with a cursor taking just the :text :width & :height values... defined as follows:

(defn svgrender
 [parent_state]
  (with-let [svgstate (atom {:clicked false})
           mousedown_handler #(swap! svgstate assoc
                                     :clicked true)
           mouseup_handler #(swap! svgstate assoc
                                   :clicked false)]
    [:svg {:viewBox "0 0 500 500"
         :width 500
         :height 500
         :id "svgcontainer"
         :onMouseDown mousedown_handler
         :onMouseUp mouseup_handler}
      [:g {:id "layer1"}
       [:rect {:id "rect1"
            :width 500
            :height 500
            :x 0
            :y 0
            :style {:fill (if (:clicked @svgstate)
                            "#ff00ff"
                            "#00ffff")}}]
         [:text {:x 5
            :y 15
            :class "small"}
          (gstring/format "width : %s height : %s" (:width @parent_state) (:height @parent_state))]
         [:text {:x 5
            :y 35
            :class "small"}
          (gstring/format "Click status : %s" (:clicked @svgstate))]
         [:text {:x 5
            :y 55
            :class "small"}
          "Parent Text:  " (:text @parent_state)]]]
        (finally
          (.removeEventListener (.getElementById js/document "svgcontainer")
                          "onmousedown" mousedown_handler)
          (.removeEventListener (.getElementById js/document "svgcontainer")
                          "onmouseup" mouseup_handler))))

The the problem is no matter what I do, I can't get the values from the parent_state to display in the child node.... they just display as nulls enter image description here

Please can someone help me? I don't know what I am doing wrong!!

UPDATE: As per suggestion from Walton I've come up with a few variations

  1. Parens call to component with full parent state passed

    (svgrender wndcomp_state)

Result : Fail

  1. Parens call to component with cursor like in the original code

    (svgrender (cursor wndcomp_state [:text :width :height]))

Result : Fail

  1. Parens call to component with track as per Walton's suggestion

    (svgrender (track #(select-keys @wndcomp_state [:text :width :height])))

Result : Pass!!

  1. Square call to component with full parent state passed

    [svgrender wndcomp_state]

Result : Pass!!

  1. Square call to component with cursor like in the original code

    [svgrender (cursor wndcomp_state [:text :width :height])]

Result : Fail

  1. Square call to component with track as per Walton's suggestion

    [svgrender (track #(select-keys @wndcomp_state [:text :width :height]))]

Result : Pass!!

Now this is so bizzare.... no? While I accept Walton's answer, can anyone explain why this is the case?


Solution

  • cursor behaves like get-in, while the code above expects it to behave like select-keys. While you could make cursor behave like your want by passing in a function as the first argument (see the second example in it's documentation) you don't need to write the cursor, so your better off using track. Something like (track #(select-keys @wndcomp_state [:text :width :height])) (untested)

    That said, in your example, even track is overkill, you really don't need a new reaction. You could just pass in the original atom to the child component and everything would would perfectly. The downside of doing this is that if your example is simplified and you have other properties that change more frequently than :width and :height you'd rerender the child necessarily every time those changed.

    Even that problem can be solved without a new reaction however. Just pass in the plain map. So instead of [svgrender (track #(select-keys @wndcomp_state [:text :width :height]))] you simply have [svgrender (select-keys @wndcomp_state [:text :width :height])] (note: I'm also using square brackets instead of parens, see here). Using this approach when wndcomp_state changes the windowdim_comp component will be re-rendered, which causes it to call the svgrender component with the text, width, and height from wndcomp_state. If these have changed, it's like calling a React component with new props, it'll be rerendered. If they haven't your calling svgrender with the same arguments it was originally rendered with. Reagent won't rerender it in that scenario (using the square brackets. With parens it'll always be rerendered).

    Same effect, but less work for Reagent/React tracking dependencies.