Reading Input With Om Next - clojurescript

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.

Related

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

Why doesn't my query get passed into my reader?

Questions
My webpage only has the output: {:user {}} with the following code.
(ns omn1.core
(:require
[om.next :as om :refer-macros [defui]]
[om.dom :as dom :refer [div]]
[goog.dom :as gdom]))
(defui MyComponent
static om/IQuery
(query [this] [:user])
Object
(render
[this]
(let [data (om/props this)]
(div nil (str data)))))
(def app-state (atom {:user {:name "Fenton"}}))
(defn reader [{q :query st :state} _ _]
(.log js/console (str "q: " q))
{:value (om/db->tree q #app-state #app-state)})
(def parser (om/parser {:read reader}))
(def reconciler
(om/reconciler
{:state app-state
:parser parser}))
(om/add-root! reconciler MyComponent (gdom/getElement "app"))
When I check the browser console, I notice that my query is nil. Why
doesn't it get passed into my reader function?
This comes from a motivation to keep my code to a minimal # of LOC as possible, and also DRY. So I'd like to have one read function that will work with a properly set up database, and normal nominal queries. If you pass regular queries to om/db->tree indeed db->tree does this. db->tree will take any proper query and return you a filled out tree of data. Maybe another way to phrase the question is can someone demonstrate a reader function that does this? I.e. leveraging db->tree to resolve the value of a query. I don't want to write a custom reader for each query I have. If all my queries obey the regular query syntax AND my DB is properly formatted, I should be able to use one reader function, no?
The example provided in the om.next quick start - thinking with links doesn't work:
(defmethod read :items
[{:keys [query state]} k _]
(let [st #state]
{:value (om/db->tree query (get st k) st)}))
as stated before query is nil sometimes, and the 2nd and 3rd arguments are different from what is proposed as how to use this function from the tests which all use: st for both 2nd and 3rd arguments. Confused.
From the Om.Next Quick Start tutorial (https://github.com/omcljs/om/wiki/Quick-Start-(om.next)), read has this signature:
[{:keys [state] :as env} key params]
So there is no access to a query data structure.
Usually the setup is to have a multimethod for each query, and use the query's params to return some part of the state:
(defmulti read (fn [env key params] key))
(defmethod read :animals/list
[{:keys [state] :as env} key {:keys [start end]}]
{:value (subvec (:animals/list #state) start end)})
Here :animals/list is the key of the query. So this is how you can access the key and params of the query.

om.next mutate of other-component state not causing other-component to re-render

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.

Why does Om Next component not re-render when state changes?

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

Cannot manipulate cursor outside of render phase

Trying react for the first time, and I want to make a simple todo list app. But every time I press enter to trigger onSubmit it says Uncaught Error: Cannot manipulate cursor outside of render phase, only om.core/transact!, om.core/update!, and cljs.core/deref operations allowed. While I think this is a very good error message, I don't know what to do.
(ns app.core
(:require [om.core :as om :include-macros true]
[sablono.core :as html :refer-macros [html]]))
(def app-state (atom
{:todos [{:todo "first"}
{:todo "second"}]
:current ""}))
(defn to-do
[data]
(om/component
(html [:li (:todo data)])))
(defn to-dos
[data]
(om/component
(html [:div
[:form {:on-submit (fn [e]
(.preventDefault e)
(om/transact! data :todos (fn [v]
(js/console.log (:current data))
(conj v (:current data)))))}
[:input {:type "text"
:placeholder "Enter some text."
:on-change (fn [e] (om/update! data :current (.. e -target -value)))}]]
[:ul
(om/build-all to-do (:todos data))]])))
(om/root to-dos app-state {:target js/document.body})
I think the problem is where you access data inside om/transact! where you should operate on v:
(:current v) instead of (:current data)
or you may try (:current #data) for most recent value of data
There are actually two issues with:
(om/transact! data :todos (fn [v]
(js/console.log (:current data))
(conj v (:current data)))))
One is what #edbond said above: you should use (:current v) rather than (:current data). The other problem, however, is that you are specifying the :todos keyword, and instead, you should simply change data itself, since :current is outside of :todos in your app-state shown. So the correct formulation would be:
(om/transact! data (fn [v]
(js/console.log (:current v))
(conj v (:current v)))))