re-frame: nvd3 graph doesn't respond to when its component's subscriptions are updated - clojurescript

I'm using the re-frame cljs framework which uses reagent as its view library. I have an nvd3 graph component that I want to be updated when its subscriptions update.
Unfortunately, the graph never updates itself after the initial call to :component-did-mount. :component-will-update is never called again after the intial render.
I want the graph to update itself as the subscription notifies the components of the dat it's listening to being changed.
Here's the graph-container component:
(defn weight-graph-container
[]
(let [weight (subscribe [:weight-change])
bodyfat (subscribe [:bodyfat-change])
weight-amount (reaction (get #weight :amount))
weight-unit (reaction (get #weight :unit))
bf-percentage (reaction (get #bodyfat :percentage))
lbm (reaction (lib/lbm #weight-amount #bf-percentage))
fat-mass (reaction (- #weight-amount #lbm))]
(reagent/create-class {:reagent-render weight-graph
:component-did-mount (draw-weight-graph #lbm #fat-mass "lb")
:display-name "weight-graph"
:component-did-update (draw-weight-graph #lbm #fat-mass "lb")})))
Here's the graph component:
(defn draw-weight-graph [lbm fat-mass unit]
(.addGraph js/nv (fn []
(let [chart (.. js/nv -models pieChart
(x #(.-label %))
(y #(.-value %))
(showLabels true))]
(let [weight-data [{:label "LBM" :value lbm} {:label "Fat Mass" :value fat-mass}]]
(.. js/d3 (select "#weight-graph svg")
(datum (clj->js weight-data))
(call chart)))))))
Finally, here's the component that the graph renders into:
(defn weight-graph []
[:section#weight-graph
[:svg]])
What am I missing? Thanks for any help.

The following code solves your problem:
(defn draw-weight-graph
[d]
(let [[lbm fat-mass unit] (reagent/children d)]
(.addGraph js/nv (fn []
(let [chart (.. js/nv -models pieChart
(x #(.-label %))
(y #(.-value %))
(showLabels true))]
(let [weight-data [{:label "LBM" :value lbm} {:label "Fat Mass" :value fat-mass}]]
(.. js/d3 (select "#weight-graph svg")
(datum (clj->js weight-data))
(call chart))))))))
(def graph-component (reagent/create-class {:reagent-render weight-graph
:component-did-mount draw-weight-graph
:display-name "weight-graph"
:component-did-update draw-weight-graph}))
(defn weight-graph-container
[]
(let [weight (subscribe [:weight-change])
bodyfat (subscribe [:bodyfat-change])
weight-amount (reaction (get #weight :amount))
weight-unit (reaction (get #weight :unit))
bf-percentage (reaction (get #bodyfat :percentage))
lbm (reaction (lib/lbm #weight-amount #bf-percentage))
fat-mass (reaction (- #weight-amount #lbm))]
(fn []
[graph-component #lbm #fat-mass "lb"])))

Related

Pass params in reframe subscriptions to a chain function clojurescript

(re-frame/reg-sub
::current-effects-list
(fn [[_ effect-type]]
[(re-frame/subscribe [::available-effects])
effect-type])
(fn [[available-effects effect-type]]
(filter (fn [{:keys [text value selected? type]}]
(= effect-type type))
available-effects)))
I want to pass the param effect-type to the next chain function as an argument, but I am new at Clojure, thus the effect-type is coming as null in the second chain function.
try this:
(re-frame/reg-sub
::current-effects-list
(fn [[_ effect-type]]
(re-frame/subscribe [::available-effects])) ; there is no need to return effect-type from here
(fn [available-effects [_ effect-type]]
(filter (fn [{:keys [text value selected? type]}]
(= effect-type type))
available-effects)))

Clojurescript re-frame subscriptions do not work

As a newbie in Clojure I'm trying my first re-frame app. It is a wizard containing sections. Each of the sections can contain one or more components. Each component could be a text-block, numeric input, ...
But if I change a value of a component in REPL, by dispatching the set-component-value event, the html doesn't get rerendered to show the updated value. However I do see in re-frisk debugger that the db gets updated.
(re-frame/dispatch [:wiz-ev/set-component-value 0 0 "TEST"])
I probably made some beginner mistake but I can't see where the problem is.
Below is a stripped down version (that compiles) that reproduces the issue.
(ns wizard.core
(:require
[reagent.dom :as rdom]
[day8.re-frame.tracing :refer-macros [fn-traced]]
[re-frame.core :as re-frame]
))
;;
;; config
;;
(def debug?
^boolean goog.DEBUG)
;;
;; DB
;;
(def default-db
{:wizard-config {
:title "title of wizard"
:sections[{:title "first section"
:components [{:title "first comp"}]}
{:title "second section"
:components[{:title "comp A"} {:title "comp B"}{:title "comp C"}]}
]
}
})
(defn create-empty-section-data
"create a vector as placeholder for component data. For each component we add a nil"
[section]
(reduce #(conj %1 nil) [] (:components section)));;
(defn add-wizard-data [db]
"add a vector :wizard-data in db. For each section a new vector end for each component a nil in the vector"
(let [sections (get-in db [:wizard-config :sections])
data (reduce #(conj %1 (create-empty-section-data %2)) [] sections)]
(assoc db :wizard-data data) ))
;;
;; events
;;
(re-frame/reg-event-db
:wiz-ev/initialize-db
(fn-traced [_ _]
( add-wizard-data default-db)))
(re-frame/reg-event-db
:wiz-ev/set-component-value
(fn-traced [db [_ section-number component-number new-value]]
(let [old-wiz-data (:wizard-data db)
new-wiz-data (assoc-in old-wiz-data [section-number component-number] new-value)]
(js/console.log "---:wiz-ev/set-component-value-------------------------------")
(js/console.log new-value)
(assoc db :wizard-data new-wiz-data))))
;;
;; subs
;;
(re-frame/reg-sub
:wiz/config
(fn[db] (:wizard-config db)) )
(re-frame/reg-sub
:wiz/section-config
(fn [_](re-frame/subscribe [:wiz/config]))
(fn [wizard-config [_ section-number]] ((:sections wizard-config) section-number) ))
(re-frame/reg-sub
:wiz/title
(fn [_] (re-frame/subscribe [:wiz/config]))
(fn [config _] (:title config)))
(re-frame/reg-sub
:wiz/section-count
(fn [_](re-frame/subscribe [:wiz/config]))
(fn [config _] (count (:sections config))))
(re-frame/reg-sub
:wiz/section-data
(fn [db [_ section-number]] ((:wizard-data db) section-number)))
(re-frame/reg-sub
:wiz/section-title
(fn [[_ section-number]] (re-frame/subscribe [:wiz/section-config section-number]) )
(fn [section-config] (:title section-config) ))
(re-frame/reg-sub
:wiz/section-components
(fn [[_ section-number]] (re-frame/subscribe [:wiz/section-config section-number]))
(fn [section-config _] (:components section-config)) )
(re-frame/reg-sub
:wiz/component-data
(fn [[_ section-number]]
(js/console.log "----[:wiz/component-data] section " (str section-number))
(re-frame/subscribe [:wiz/section-data section-number]) )
(fn [section_data [_ _ component-number]]
;;(fn [section_data [_ _ par2]]
;;(fn [par2]
(js/console.log "----[:wiz/component-data] comp funct, component=" (str component-number))
;;(js/console.log "----[:wiz/component-data] component " component-number " from section " sect-nbr)
;; (section_data component-number)
)
)
;;
;; view
;;
(defn render-component
[component component-number section-number]
(let [
value #(re-frame/subscribe [:wiz/component-data section-number component-number])]
;;(case (:type component)
;; :text (render-component-text component component-number section-number)
;; :memo (render-component-memo component component-number section-number)
;; (render-component-default component)
;; )
[:div "The VALUE for component " component-number " (section" section-number") is : " value]
)
)
(defn render-section
[section-number]
(let [title #(re-frame/subscribe [:wiz/section-title section-number])
components #(re-frame/subscribe [:wiz/section-components section-number])]
[:div
[:h2 title]
(into [:div] (map-indexed #(render-component %2 % section-number) components))]))
(defn main-panel []
(let [wizard-title #(re-frame/subscribe [:wiz/title])
section-count #(re-frame/subscribe [:wiz/section-count])
]
[:div
[:h1 wizard-title]
(into [:<>] (map #(vector render-section %) (range section-count)))
]))
;;
;;core
;;
(defn dev-setup []
(when debug?
(println "*** dev mode ***")))
(defn ^:dev/after-load mount-root []
(re-frame/clear-subscription-cache!)
(let [root-el (.getElementById js/document "app")]
(rdom/unmount-component-at-node root-el)
(rdom/render [main-panel] root-el)))
(defn init []
(re-frame/dispatch-sync [:wiz-ev/initialize-db])
(dev-setup)
(mount-root) ; render to element 'app' the view main-panel
)
Solved,
1st, I commented out a bit too much while debugging the component-data subscription
2nd, the function parameter of component-data was not right
(re-frame/reg-sub
:wiz/component-data
(fn [[_ section-number _]] ;; ---- FIX NBR 2
(js/console.log "----[:wiz/component-data] section " (str section-number))
(re-frame/subscribe [:wiz/section-data section-number]) )
(fn [section-data [_ _ component-number]]
(js/console.log "----[:wiz/component-data]" )
(js/console.log "component=" (str component-number))
(js/console.log "data " (str section-data))
(section-data component-number) ;; ---- FIX NBR 1
)
)

Reagent Component not Re-Rendering on Prop Change

My Reagent component ist a simple div that has a component-did-mount and a component-did-update hook. It draws notes using vexflow.
(defn note-bar [notes]
(reagent/create-class
{:display-name "Note Bar"
:reagent-render (fn [notes]
^{:key notes} ;; force update
[:div#note-bar])
:component-did-mount (fn [this]
(draw-system-with-chord notes))
:component-did-update (fn [this]
(draw-system-with-chord notes))}))
It is used like this.
(defn exercise-one []
(let [note (re-frame/subscribe [:exercise-one/note])]
[:div
[note-bar/note-bar #note]
[other]
[components]]))
My event code is the following.
(defn store-exercise-one-note [db [_ note]]
(assoc-in db [:exercise-one :note-bar :note] note))
(re-frame/reg-event-db
:exercise-one/store-note
store-exercise-one-note)
(defn query-exercise-one-note [db]
(or (get-in db [:exercise-one :note-bar :note])
[{:octave 4 :key :c}]))
(re-frame/reg-sub
:exercise-one/note
query-exercise-one-note)
I verified that the app-db value changes using 10x. Yet the note bar only displays a different note when Hot Reloading kicks in. I believe this is due to the component-did-update hook not being called.
My question is, is this the right way to bind a JavaScript library that renders something? If so, why does my component not update?
The following fixed the component. See the documentation about form-3 components here
(defn note-bar [notes]
(reagent/create-class
{:display-name "Note Bar"
:reagent-render (fn [notes]
^{:key notes} ;; force update
[:div#note-bar])
:component-did-mount (fn []
(draw-system-with-chord notes))
:component-did-update (fn [this]
(let [new-notes (rest (reagent/argv this))]
(apply draw-system-with-chord new-notes)))}))

Let Sub Component React on Parent's State Plus Having its Own State

Consider the following Reagent components:
(defn sub-compo [n]
(let [state (r/atom {:colors (cycle [:red :green])})]
(fn []
[:div {:style {:color (-> #state :colors first)}
:on-mouse-move #(swap! state update :colors rest)}
"a very colorful representation of our number " n "."])))
(defn compo []
(let [state (r/atom {:n 0})]
(fn []
[:div {:on-click #(swap! state update :n inc)}
"Number is " (#state :n) "."
[sub-compo (#state :n)]])))
I tried to make up an example, in which a sub component should depend on the state of its parent component. However the sub component should have an internal state as well. The above does not work properly. When the state in compo changes sub-compo is not initialized a new.
Which would be the way to go here, in order to let sub-compo be in sync with comp? Whenever the state of comp changes sub-comp should actually be initialized anew, meaning it's color state is set to the initial value again.
Here's a solution that does at least what I want. It uses a cursor and a watch. But maybe there is a simpler way to do so, anyways:
(defn sub-compo [n]
(let [init-state {:colors (cycle [:red :green])}
state (r/atom init-state)]
(add-watch n :my (fn []
(reset! state init-state)))
(fn []
[:div {:style {:color (-> #state :colors first)}
:on-mouse-move #(swap! state update :colors rest)}
"a very colorful representation of our number " #n "."])))
(defn compo []
(let [state (r/atom {:n 0})]
(fn []
[:div {:on-click #(swap! state update :n inc)}
"Number is " (#state :n) "."
[sub-compo (r/cursor state [:n])]])))
The above does not work properly. When the state in compo changes
sub-compo is not initialized a new.
This is because the inner function of sub-compo needs to receive the argument n as well.
Whenever the state of comp changes sub-comp should actually be
initialized anew, meaning it's color state is set to the initial value
again.
You could use :component-will-receive-props for this.
This worked for me:
(defn sub-compo [n]
(let [init {:colors (cycle [:red :green])}
state (r/atom init)]
(r/create-class
{:component-will-receive-props
(fn [this [_ n]]
(reset! state init))
:reagent-render
(fn [n]
[:div {:style {:color (-> #state :colors first)}
:on-mouse-move #(swap! state update :colors rest)}
"a very colorful representation of our number " n "."])})))

How to override onload methods in ClojureScript?

I am trying to override onload function of document and Image in ClojureScript. I think that set! should be possible to do it, but i am not getting any success. Relevant code is as follows :
(defn load-image [img-path]
(let [img (js/Image.)]
(do (set! (.-src img) img-path)
img)))
(defn add-img-canvas [img-path width height]
(let [img (load-image img-path)]
(set! (.-onload img)
(fn [] ;; This function is never called.
(let [canvas (get-scaled-canvas img width height)]
(do (pr-str canvas)
(swap! game-state :canvas canvas)))))))
(defn hello-world []
(let [count (atom 1)]
(fn []
[:div
[:h1 (:text #game-state)]
[:div (do (swap! count inc) (str "count is " #count))]
[:canvas (:canvas #game-state)]])))
(reagent/render-component [hello-world]
(. js/document (getElementById "app")))
(set! (.-onload js/document)
(fn [] ;; This function is also never called.
(add-img-canvas (:img-src game-state) 100 130)))
;;(. js/document onload)
Anonymous functions in add-img-canvas is not getting called. What am i doing wrong ?
I think it may be down to the difference between document.onload vs window.onload. The latter does work as expected.
See this for more details between the two.