Clojurescript re-frame subscriptions do not work - clojurescript

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
)
)

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)))

form-3 component not rerendering anything even though :component-did-update is called

I have the following code to test out form-3 components:
(defn inner [data]
(reagent/create-class
{:display-name "Counter"
:component-did-mount (fn []
(js/console.log "Initialized")
[:h1 "Initialized! " data])
:component-did-update (fn [this _]
(let [[_ data] (reagent/argv this)]
(js/console.log (str "Updated " data))
[:div (str "My clicks " data)]))
:reagent-render (fn [] [:div (str "My clicks " data)])}))
I am able to successfully trigger both the :component-did-mount and the :component-did-update as there is the expected output in the console log. However, neither of the two functions actually change anything on the page. It just shows the initial state of [:div (str "My clicks " data)] the whole time.
What am I doing wrong? Ps. I have read the reagent docs and the purelyfunctional guide.
You have to repeat the parameters in the :reagent-render function:
(fn [data] [:div (str "My clicks " data)])

Get all of named routes

I am currently learning reagent with secretary as its route. I find that I can use query-params to get a hash-map of all parameters with question mark (?) like ?name=Daniel
(ns feampersanda.core
(:require-macros [secretary.core :refer [defroute]])
(:import goog.History)
(:require
[secretary.core :as secretary]
[goog.events :as events]
[goog.history.EventType :as EventType]
[reagent.core :as r]))
;; ------------------------------
;; States
;; page --> is occupied by page state
(def app-state (r/atom {:params {}}))
;; ------------------------------
;; History
(defn hook-browser-navigation! []
(doto (History.)
(events/listen
EventType/NAVIGATE
(fn [event]
(secretary/dispatch! (.-token event))))
(.setEnabled true)))
;; -------------------------
;; Views
;; -------------------------
;; Parameters
(defn update-query-params! [query-params]
(do
(js/console.log (str query-params))
(swap! app-state assoc-in [:params :query] query-params))
)
;; -------------------------
;; Routing Config
(defn app-routes []
(secretary/set-config! :prefix "#")
(defroute "/" [query-params]
(do
(update-query-params! query-params)
(swap! app-state assoc :page :home)))
(defroute "/about/:id" [id query-params]
(do
(js/console.log id)
(update-query-params! query-params)
(swap! app-state assoc :page :about)))
(hook-browser-navigation!))
(defmulti current-page #(#app-state :page))
(defmethod current-page :home []
[:div [:h1 (str "Home Page")]
[:a {:href "#/about"} "about page"]
[:br]
[:a {:href "#/about"} (str (:count #app-state))]
])
(defmethod current-page :about []
[:div [:h1 "About Page"]
[:a {:href "#/"} (str "home page" " --> "
(:yes (:query (:params #app-state)))
)]])
(defmethod current-page :default []
[:div
[:p "404"]
])
;; -------------------------
;; Initialize app
(defn mount-root []
(app-routes)
(r/render [current-page] (.getElementById js/document "app")))
(defn init! []
(mount-root))
I don't know how to pass the id parameter to a defmethod, so I want it to be saved inside an atom, so I wonder how to get hash-map which is include all of the named parameters like http://0.0.0.0:3449/#/about/black/12 to {:path "black" :id "12"}
One solution would be to use cemerick's URL library
(require '[cemerick.url :as url])
(keys (:query (url/url (-> js/window .-location .-href))))
https://github.com/cemerick/url

Om Next subquery doesn't have effect on sub component props

Reading this Om Next tutorial page Components, Identity & Normalization, I thought the subquery from the subcomponent (Person component) is used to populate the Person's props. But changing the query from
'[:name :points :age]
to
'[]
doesn't break the app. Could you help me understand how the parser invokes read methods from these component/query tree.
The entire code from the page is below.
(def init-data
{:list/one [{:name "John" :points 0}
{:name "Mary" :points 0}
{:name "Bob" :points 0}]
:list/two [{:name "Mary" :points 0 :age 27}
{:name "Gwen" :points 0}
{:name "Jeff" :points 0}]})
;; -----------------------------------------------------------------------------
;; Parsing
(defmulti read om/dispatch)
(defn get-people [state key]
(let [st #state]
(into [] (map #(get-in st %)) (get st key))))
(defmethod read :list/one
[{:keys [state] :as env} key params]
{:value (get-people state key)})
(defmethod read :list/two
[{:keys [state] :as env} key params]
{:value (get-people state key)})
(defmulti mutate om/dispatch)
(defmethod mutate 'points/increment
[{:keys [state]} _ {:keys [name]}]
{:action
(fn []
(swap! state update-in
[:person/by-name name :points]
inc))})
(defmethod mutate 'points/decrement
[{:keys [state]} _ {:keys [name]}]
{:action
(fn []
(swap! state update-in
[:person/by-name name :points]
#(let [n (dec %)] (if (neg? n) 0 n))))})
;; -----------------------------------------------------------------------------
;; Components
(defui Person
static om/Ident
(ident [this {:keys [name]}]
[:person/by-name name])
static om/IQuery
(query [this]
'[])
Object
(render [this]
(println "Render Person" (-> this om/props :name))
(let [{:keys [points name age] :as props} (om/props this)]
(dom/li nil
(dom/label nil (str name ", points: " points ", age: " age))
(dom/button
#js {:onClick
(fn [e]
(om/transact! this
`[(points/increment ~props)]))}
"+")
(dom/button
#js {:onClick
(fn [e]
(om/transact! this
`[(points/decrement ~props)]))}
"-")))))
(def person (om/factory Person {:keyfn :name}))
(defui ListView
Object
(render [this]
(println "Render ListView" (-> this om/path first))
(let [list (om/props this)]
(apply dom/ul nil
(map person list)))))
(def list-view (om/factory ListView))
(defui RootView
static om/IQuery
(query [this]
(let [subquery (om/get-query Person)]
`[{:list/one ~subquery} {:list/two ~subquery}]))
Object
(render [this]
(println "Render RootView")
(let [{:keys [list/one list/two]} (om/props this)]
(apply dom/div nil
[(dom/h2 nil "List A")
(list-view one)
(dom/h2 nil "List B")
(list-view two)]))))
(def reconciler
(om/reconciler
{:state init-data
:parser (om/parser {:read read :mutate mutate})}))
(om/add-root! reconciler
RootView (gdom/getElement "app"))
From reading this now I know that only the top level queries are processed by the parser. And you are responsible for providing your own reads for subqueries by accessing (:query env).

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

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"])))