Get all of named routes - clojurescript

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

Related

Layout component unable to render when Reitit router introduced

I'm having trouble sorting out why a layout/parent component (app.components.layout) is not resolving after a hard reload BUT is okay after a hot reload with dev server after I introduced routes.
The error: TypeError: dapp.components.layout.render is not a function
This error doesn't appear when I render the view component in the #match atom directly.
(ns app.views
(:require
[reagent.core :as r]
[reitit.frontend :as rf]
[reitit.coercion.schema :as rsc]
[dapp.components.layout :as layout]
[dapp.components.dashboard :as dashboard]
[dapp.components.settings :as settings])))
(def routes
(rf/router
["/"
[""
{:name ::dashboard
:view dashboard/render
:controllers [{:start (log-fn "start" "dashboad controller")
:stop (log-fn "stop" "dashboard controller")}]}]
["settings"
{:name ::settings
:view settings/render}]]
{:data {:controllers [{:start (log-fn "start" "root-controller")
:stop (log-fn "stop" "root controller")}]
:coercion rsc/coercion}}))
;; broken on a hard browser refresh but works when shadow does a hot reload.
(defn main-panel []
[:div
(if #match
(let [view (:view (:data #match))]
(layout/render view #match)))]) ;; TypeError: app.components.layout.render is not a function
;; working
(defn main-panel []
[:div
(if #match
(let [view (:view (:data #match))]
(view nil))])
Layout Component:
(ns app.components.layout)
(defn render [view match]
[:div
[view]])
View Component:
(ns app.components.dashboard)
(defn render [props]
[:div
"Dashboard"])
Setup:
(ns app.core
(:require
[reagent.dom :as rdom]
[re-frame.core :as re-frame]
[reitit.frontend.easy :as rfe]
[reitit.frontend.controllers :as rfc]
[dapp.events :as events]
[dapp.views :as views]
[dapp.config :as config]
))
(defn dev-setup []
(when config/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 [views/main-panel] root-el)))
(defn init []
(re-frame/dispatch-sync [::events/initialize-db])
(dev-setup)
;; router setup
(rfe/start!
views/routes
(fn [new-match]
(swap! views/match (fn [old-match]
(if new-match
(assoc new-match :controllers (rfc/apply-controllers (:controllers old-match) new-match))))))
{:use-fragment true})
(mount-root))

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

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

Om ref cursor not re-rendering components when updated

(ns ^:figwheel-always refs-test.core
(:require [om.core :as om :include-macros true]
[om.dom :as dom :include-macros true]
[sablono.core :as html :refer-macros [html]]))
(enable-console-print!)
(def app-state
(atom {:items [{:text "cat"}
{:text "dog"}
{:text "bird"}]
:selected-item {}}))
(defn selected-item []
(om/ref-cursor (:selected-item (om/root-cursor app-state))))
(defn
selected-item-title
[_ owner]
(reify
om/IRender
(render [_]
(html
[:div
(let [selected (om/observe owner (selected-item))]
(if (empty? selected)
[:h1 "Nothing selected"]
[:h1 (:text selected)]))]))))
(defn
selected-item-button
[item owner]
(reify
om/IRender
(render [_]
(html
[:li
[:button {:on-click
(fn []
(om/update! (om/root-cursor app-state) :selected-item item) ;; this doesn't update
;;(om/update! (om/root-cursor app-state) :selected-item (merge item {:foo 1})) ;; this does
)} (:text item)]]))))
(defn
root
[cursor owner]
(reify
om/IRender
(render [_]
(html
[:div
(om/build selected-item-title {})
[:ul
(om/build-all selected-item-button (:items cursor))]]))))
(om/root root app-state
{:target (.getElementById js/document "app")})
(https://www.refheap.com/108491)
The (selected-item) function crerates a ref-cursor which tracks the :selected-item key in app-state. When you click a selected-item-button the title changes to reflect the new value that has been put into the map. However, this only works once. Pressing a different button does not cause the title to re-render again so the title is always stuck at the value of the first button you pressed.
Although, simply adding a merge with an additional keyword seems to make it work... (merging with an empty map doesn't work either, tried that!)
Is my understanding on ref cursors wrong?
So, the issue was very simple.
(om/update! (om/root-cursor app-state) :selected-item item)
should have been
(om/update! (om/root-cursor app-state) :selected-item #item)
Notice the item, because it's a cursor, is dereferenced.

transact app-state in will-mount has no effect

This question can be best explained with an example:
;; create a basic om app.
lein new mies-om om-tut
lein cljsbuild auto.
Then paste in the following code (in core.cljs)
(ns om-tut.core
(:require [om.core :as om :include-macros true]
[om.dom :as dom :include-macros true]))
(def app-state (atom {:text "Hello world!"}))
(om/root
(fn [app owner]
(reify
om/IWillMount
(will-mount [_]
(om/update! app :text "Success!!!"))
om/IRender
(render [_]
(dom/div nil (app :text ))
)))
app-state
{:target (. js/document (getElementById "app"))})
The code in will-mount is actually being executed, if you drop in a println function, then you'll see that. What is not clear is why the rendering loop is called only once. On the other hand, if you wrap the om/update! within a go block, then it works as expected:
;; add [org.clojure/core.async "0.1.346.0-17112a-alpha"] to your deps in project.clj
(ns om-tut.core
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [om.core :as om :include-macros true]
[cljs.core.async :refer [put! chan <! to-chan close!]]
[om.dom :as dom :include-macros true]))
(def app-state (atom {:text "Hello world!"}))
(om/root
(fn [app owner]
(reify
om/IWillMount
(will-mount [_]
(go
(om/update! app :text "Success!!")))
om/IRender
(render [_]
(dom/div nil (app :text )))))
app-state
{:target (. js/document (getElementById "app"))})
The question is: Why does will-mount not trigger a new rendering loop, since I update app state? I like to use go blocks when I need them, but I don't see why I am forced to wrap this simple example in a block.
It think that will-mount is not a good place to update cursor.
Calling om/build with the :fn option will do what you're trying to achieve.
Component is rendered only once, with the updated cursor.
(om/build mycomponent data {:fn #(assoc % :text "Success !")})
https://github.com/swannodette/om/wiki/Documentation#build