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)))))
Related
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.
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
First of all, this https://github.com/taylorSando/om-material-ui doesn't work with latest React/Material UI.
The main reason, I think, is this warning in console:
Warning: Something is calling a React component directly. Use a factory or JSX instead. See: https://fb.me/react-legacyfactory
I've also tried to create component "manually":
(ns om-test.core
(:require [om.core :as om :include-macros true]
[om-tools.dom :as dom :include-macros true]
[om-tools.core :refer-macros [defcomponent]]
[om-material-ui.core :as mui :include-macros true]))
(enable-console-print!)
(defonce app-state (atom {:text "Hello Chestnut!"}))
(defn main []
(om/root
(fn [app owner]
(reify
om/IRender
(render [_]
(dom/div (dom/element js/MaterialUI.Paper {} "Hello")
(mui/paper {} "Hello"))
)))
app-state
{:target (. js/document (getElementById "app"))}))
So, both of these approaches produces same warning above.
There has been obviously some changes with React. It suggests to create components programatically as:
var React = require('react');
var MyComponent = React.createFactory(require('MyComponent'));
function render() {
return MyComponent({ foo: 'bar' });
}
So how do I create Material UI component inside Om render function, or maybe better How do I create React component inside Om render function, in general?
By Material UI I mean this https://github.com/callemall/material-ui
My dependencies
:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/clojurescript "0.0-3058" :scope "provided"]
[ring "1.3.2"]
[ring/ring-defaults "0.1.4"]
[compojure "1.3.2"]
[enlive "1.1.6"]
[org.omcljs/om "0.9.0"]
[environ "1.0.0"]
[http-kit "2.1.19"]
[prismatic/om-tools "0.3.11"]
[om-material-ui "0.1.1" :exclusions [org.clojure/clojurescript
org.clojure/clojure]]]
Okay I eventually figured out.
Build latest version of Material UI with this: https://github.com/taylorSando/om-material-ui/tree/master/build-mui. Note: No need to build CSS in current version (0.10.4)
Include built material.js into your HTML file. Again, no need to include CSS.
Avoid loading React twice https://github.com/taylorSando/om-material-ui#avoid-loading-react-twice
Now the code for Om:
(ns material-ui-test.core
(:require [om.core :as om :include-macros true]
[om.dom :as dom :include-macros true]))
(enable-console-print!)
(defonce app-state (atom {:text "Hello Chestnut!"}))
(def ^:dynamic *mui-theme*
(.getCurrentTheme (js/MaterialUI.Styles.ThemeManager.)))
(defn main []
(om/root
(fn [app owner]
(reify
om/IRender
(render [_]
(let [ctor (js/React.createFactory
(js/React.createClass
#js
{:getDisplayName (fn [] "muiroot-context")
:childContextTypes #js {:muiTheme js/React.PropTypes.object}
:getChildContext (fn [] #js {:muiTheme *mui-theme*})
:render (fn []
(dom/div nil
(dom/h1 nil (:text app))
(js/React.createElement js/MaterialUI.Slider)))}))]
(ctor. nil)))))
app-state
{:target (. js/document (getElementById "app"))}))
If you used just (js/React.createElement js/MaterialUI.Slider) without :getChildContext etc. it would throw error:
Uncaught TypeError: Cannot read property 'component' of undefined
This is because of how current MaterialUI works. Read "Usage" part here: http://material-ui.com/#/customization/themes
Code for Reagent is bit more elegant. But I've used here namespace
[material-ui.core :as ui :include-macros true]
copy-pasted from this example project: https://github.com/tuhlmann/reagent-material
(def ^:dynamic *mui-theme*
(.getCurrentTheme (js/MaterialUI.Styles.ThemeManager.)))
(defn main-panel []
(let [active-panel (rf/subscribe [:active-panel])]
(r/create-class
{:display-name "Main Panel"
:child-context-types
#js {:muiTheme js/React.PropTypes.object}
:get-child-context
(fn [this]
#js {:muiTheme *mui-theme*})
:reagent-render
(fn []
[ui/Slider {:name "slide1"}])})))
EDIT: I released library, which greatly simplifies whole process.
Library: https://github.com/madvas/cljs-react-material-ui
Example app: https://github.com/madvas/cljs-react-material-ui-example
I'm not using Material UI but React Widgets. Here is the wrapper I needed to write for om:
(defn dropdown-list
[data owner {:keys [val-key menu-key id-key label-key props]}]
(reify
om/IRender
(render [_]
(let [menu (-get data menu-key)]
(js/React.createElement js/ReactWidgets.DropdownList
(-> {:defaultValue (-> (find-by-key menu id-key (-get data val-key))
(-get label-key))
:data (mapv #(-get % label-key) menu)
:onChange (fn [new-val]
(let [new-id (-> (find-by-key menu label- key new-val)
(-get id-key))]
(om/update! data val-key new-id)))}
(merge props)
clj->js))))))
So, in general, you need to get the React class (js/ReactWidgets.DropdownList) and call js/Readt.createElement while passing the props on render.
I have the following ClojureScript code that uses the om library as a wrapper to React.js
(defn list-view [app owner]
(reify
om/IInitState
(init-state [_]
{:filter nil
:selected-domain nil})
om/IWillMount
(will-mount [_]
(th/poll filter-chan (fn[data]
(om/set-state! owner :filter data))))
om/IRenderState
(render-state [this state]
(let [list-data (sort (list-data (:list app) (:filter state)))]
(if (> (count list-data) 0)
(dom/div #js {:className "sidebar-module sidebar-module-inset"}
(dom/div #js {:className "bs-example well"}
(apply dom/ul #js {:className "list-group"}
(map (fn [text] (domain-list-item text (:selected-domain state) owner))
list-data))))
(dom/span nil))))))
This are the helper functions used in the code above
(defn list-data [alist filter-text ]
(filter (fn [x] (cond (empty? filter-text) false
(nil? filter-text) false
(= filter-text "*") true
:else (> (.indexOf (.toLowerCase x) filter-text) -1))) alist))
(defn domain-list-item [text selected owner]
(let [class-name (str "list-group-item" (if (= text selected) " isSelected" ""))]
(dom/li #js {:className class-name}
(dom/a #js
{:href "#"
:onClick (fn [event] (select-domain owner text))} text))))
Everything works as expected. The only thing that bothers me Is that I do not see any state info when I analyze the page with the React.js tools in Chrome.
It seems as if sometimes the state is directly visible in the state area (e.g. for an input element) and sometimes it is hidden inside the __om_state object.
I find that I often have to click on another component and then back on the component I'm interested in to see the state in the state area. Perhaps that's the issue here?