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).
Related
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
I am updating state in one of my mutations, and a piece of it is not used by this component, but is by another one. When I do the mutate I see the that the app-state is updated in the repl, and if I cause the component to re-render for other reasons, it will show correctly, but I can not get the mutate to schedule a re-render of the second component. In the example below clicking on a button should decrement the value near the color name in the second list, but it does not.
There is some examples showing using :value [k k] in the mutate return, but those throw an error, must be out of date tutorials, as the current format is :value {:keys [...]}, so says the code and some tutorials . However I can't find any part of om.next actually USING :keys as a keyword that isn't a destructure operation (so not using :keys as an actual keyword, but it is a common word so I may have missed one somewhere)
In the repl I see this for the app-state:
=> (om/app-state reconciler)
#object [cljs.core.Atom {:val
{:tiles [[:tile/by-pos "a7"]
[:tile/by-pos "a9"]
[:tile/by-pos "a11"]],
:inventory [[:inv/by-color "red"]
[:inv/by-color "blue"]
[:inv/by-color "green"]],
:tile/by-pos {"a7" {:pos "a7", :color nil},
"a9" {:pos "a9", :color nil},
"a11" {:pos "a11", :color nil}},
:inv/by-color {"red" {:color "red", :remaining 2},
"blue" {:color "blue", :remaining 1},
"green" {:color "green", :remaining 1}}}}]
What am I missing?
(ns omnexttest.core
(:require [goog.dom :as gdom]
[om.next :as om :refer-macros [defui]]
[om.dom :as dom]))
(defmulti read om/dispatch)
(defmethod read :default
[{:keys [state] :as env} key params]
(let [st #state ]
(if-let [[_ value] (find st key)]
{:value value}
{:value :not-found})))
(defmethod read :tiles
[{:keys [state] :as env} key params]
{:value (into [] (map #(get-in #state %) (get #state key))) })
(defmethod read :inventory
[{:keys [state] :as env} key params]
{:value (into [] (map #(get-in #state %) (get #state key))) })
(defmulti mutate om/dispatch)
(defmethod mutate 'draw/edit-edge
[{:keys [state] :as env} _ {:keys [this pos color]}]
{:value {:keys [[:inv/by-color color :remaining]]}
:action (fn [] (do
(swap! state assoc-in [:tile/by-pos pos :color] color )
(swap! state update-in [:inv/by-color color :remaining] dec)))})
(defn hex-color
[ this pos color ]
(om/transact! this `[(draw/edit-edge ~{:this this :pos pos :color color})]))
(defui TileView
static om/Ident
(ident [this {:keys [pos]}] [:tile/by-pos pos])
static om/IQuery
(query [this] '[:pos :color])
Object
(render [this]
(let [{:keys [pos color] :as props} (om/props this)]
(dom/li nil
(str pos " " color)
(for [color ["red" "green" "blue"]]
(dom/button #js { :onClick (fn [e] (hex-color this pos color)) }
color))))))
(def tile-view (om/factory TileView {:keyfn :pos}))
(defui InvView
static om/Ident
(ident [this {:keys [color]}] [:inv/by-color color])
static om/IQuery
(query [this] '[:color :remaining])
Object
(render [this]
(let [{:keys [color remaining] :as props} (om/props this) ]
(dom/li nil (str color " " remaining)))))
(def inv-view (om/factory InvView {:keyfn :color}))
(def app-state {
:tiles [{ :pos "a7" :color nil }
{ :pos "a9" :color nil }
{ :pos "a11" :color nil }
]
:inventory [{ :color "red" :remaining 2}
{ :color "blue" :remaining 1}
{ :color "green" :remaining 1}]
})
(defui MapView
static om/IQuery
(query [this]
[{:tiles (om/get-query TileView)}
{:inventory (om/get-query InvView) }])
Object
(render [this]
(let [tiles (-> this om/props :tiles)
inv (-> this om/props :inventory) ]
(dom/div nil
(dom/ul nil
(mapv tile-view tiles))
(dom/ul nil
(mapv inv-view inv))))))
(def reconciler
(om/reconciler
{:state app-state
:parser (om/parser {:read read :mutate mutate})}))
(om/add-root! reconciler
MapView (gdom/getElement "map"))
(defn on-js-reload []
;; optionally touch your app-state to force rerendering depending on
;; your application
;; (swap! app-state update-in [:__figwheel_counter] inc)
)
The this that is passed into om/transact! is important for re-rendering, so here if this was for a MapView component then all three components would be re-rendered. You can have the function in MapView (thus using MapView's this) but call it from TileView. In TileView's render you need something like this:
{:keys [click-cb-fn]} (om/get-computed this)
When you call om/transact! re-rendering is done down from the component you pass as first argument - this. Thus, to take this to its extreme, you'll never have re-rendering problems if all om/transacts!s are done from the root component, and all functions are passed down via computed props.
But you don't have to pass functions down. An alternative is to keep them at the same component where the firing button is, and instead pass down (again via computed props) the parent component's this. All that matters is what component the first argument to om/transact! is - call om/transact! from where ever you like.
Follow on reads are another thing to be considered when thinking about re-rendering, but not for the example you gave - they are best considered when the component you need to be re-rendered is in a different subbranch of the render tree, where using a common root's this would not be practical.
Another thing to note is that a mutate's value is 'just for documentation'. So whatever you put there will have no effect.
I'm trying to understand how to read state from a text box in om.next. As I understand it, we are no longer bound/supposed to use core.async.
As a small example, consider writing in a textbox and binding it to a paragraph element, so that the text you enter automatically appears on the screen.
(def app-state (atom {:input-text "starting text"}))
(defn read-fn
[{:keys [state] :as env} key params]
(let [st #state]
(if-let [[_ v] (find st key)]
{:value v}
{:value :not-found})))
(defn mutate-fn
[{:keys [state] :as env} key {:keys [mytext]}]
(if (= 'update-text key)
{:value {:keys [:input-text]}
:action
(fn []
(swap! state assoc :input-text mytext))}
{:value :not-found}))
(defui RootView
static om/IQuery
(query [_]
[:input-text])
Object
(render [_]
(let [{:keys [input-text]} (om/props _)]
(dom/div nil
(dom/input
#js {:id "mybox"
:type "text"
:value input-text
:onChange #(om/transact! _ '[(update-text {:mytext (.-value (gdom/getElement "mybox"))})])
})
(dom/p nil input-text)))))
This doesn't work.
When firing the onChange event in the input form, the quoted expression does not grab the text from the box.
The first mutation fires and updates, but then subsequent mutations are not fired. Even though the state doesn't changed, should the query read the string from app-state and force the text to be the same?
I would make the :onChange event look like this:
:onChange (fn (_)
(let [v (.-value (gdom/getElement "mybox"))]
#(om/transact! this `[(update-text {:mytext ~v})])))
Here the value v will actually be going through. But also om/transact! needs either a component or the reconciler to be passed as its first parameter. Here I'm passing in this which will be the root component.
This does not seem to be happening as the Quick Start tutorial says:
In Om Next application state changes are managed by a reconciler. The
reconciler accepts novelty, merges it into the application state,
finds all affected components based on their declared queries, and
schedules a re-render.
When I change the select box the mutate function updates the state, but the App component's render function never executes. I can see with #app-state in the REPL that the state has changed and I never see the output in the console from the prn in the App's render function. This is all I see in the console:
[1955.847s] [om.next] transacted '[(om-tutorial.core/switch-topic {:name "b"})], #uuid "c3ba6741-81ea-4cbb-8db1-e86eec26b540"
"read :default" :topics
If I update the state from the REPL with (swap! app-state update-in [:current-topic] (fn [] "b")) then the App's render function does execute. Here is the console output:
"read :default" :topics
"read :default" :current-topic
"App om-props " {:topics [{:name "a"} {:name "b"}], :current-topic "b"}
"Topics om-props " {:topics [{:name "a"} {:name "b"}]}
Here is the full code:
(ns om-tutorial.core
(:require [goog.dom :as gdom]
[om.next :as om :refer-macros [defui]]
[om.dom :as dom]))
(enable-console-print!)
(def app-state (atom {:current-topic "a" :topics [{:name "a"} {:name "b"}]}))
(defmulti read (fn [env key params] key))
(defmethod read :default
[{:keys [state] :as env} key params]
(prn "read :default" key)
(let [st #state]
(if-let [value (st key)]
{:value value}
{:value :not-found})))
(defmulti mutate om/dispatch)
(defmethod mutate 'om-tutorial.core/switch-topic
[{:keys [state]} _ {:keys [name]}]
{:action
(fn []
(swap! state update-in
[:current-topic]
#(identity name)))})
(defui Topics
static om/IQuery
(query [this]
[:topics])
Object
(render [this]
(let [{:keys [topics] :as props} (om/props this)]
(prn "Topics om-props " props)
(apply dom/select #js {:id "topics"
:onChange
(fn [e]
(om/transact! this
`[(switch-topic ~{:name (.. e -target -value)})]))}
(map #(dom/option nil (:name %)) topics)))))
(def topics-view (om/factory Topics))
(defui App
static om/IQuery
(query [this]
'[:topics :current-topic])
Object
(render [this]
(let [{:keys [topics current-topic] :as om-props} (om/props this)]
(prn "App om-props " om-props)
(dom/div nil
(topics-view {:topics topics})
(dom/h3 nil current-topic)))))
(def reconciler
(om/reconciler
{:state app-state
:parser (om/parser {:read read :mutate mutate})}))
(om/add-root! reconciler App (gdom/getElement "app"))
Here is the project.clj file:
(defproject om-tutorial "0.1.0-SNAPSHOT"
:description "My first Om program!"
:dependencies [[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.170"]
[org.omcljs/om "1.0.0-alpha24"]
[figwheel-sidecar "0.5.0-SNAPSHOT" :scope "test"]])
I had the same issue in my application and found a workaround (although this might not be the best solution). You can construct your components by passing the om properties of the parent component.
Your ui App could would then look like this:
(defui App
Object
(render [this]
(dom/div nil (topics-view (om/props this)))))
IQuery is definitely the better solution, but I still have the same issue like you. This workaround works in my projects for now and I will definitely take a look at IQuery again.
Edit
The tutorial about Components, Identity and Normalization explains what you have to do to update the UI when it is necessary. This results in a more idiomatic solution.
Om Next is reluctant about triggering re-reads on queries for performance reasons, in order to avoid calling the read functions for them unnecessarily and avoid useless re-renders. To specify that components that query :current-topic should re-render (and the relevant read function called), you can provide these keys in the end of the transact vector:
(om/transact! this
`[(switch-topic ~{:name (.. e -target -value)})
:current-topic])
Reference: https://github.com/omcljs/om/wiki/Documentation-(om.next)#transact
(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.