I'm displaying a menu in Om, using a component and subcomponent like this:
(def app-state (atom {:location ""
:menuitems [["Pages" "/pages/"]
["Images" "/images/"]]}))
(defn menu-item-view [parent-cursor item owner]
(reify
om/IRender
(render [this]
(dom/li #js {:className (if (= (:location #app-state) (last item)) "active" "inactive")}
(dom/a #js
{:onClick (fn [_] (swap! app-state assoc :location (last #item)))}
(first item))))))
(defn menu-view [app owner]
(reify
om/IRender
(render [this]
(dom/li #js {:className "has-dropdown not-click"}
(dom/a nil "Menu")
(apply dom/ul #js {:className "dropdown"}
(om/build-all (partial menu-item-view app)
(:menuitems app)))))))
(om/root menu-view app-state
{:target (. js/document (getElementById "menu"))})
My question is how do I update the (#app-state :location) and correctly rerender the menu?
The update in the code above:
(swap! app-state assoc :location (last #item))
does work, but the tree is not updated correct.
I suspect i need to use om/update! or om/transact! but they take a cursor and the only cursor i have in menu-item-view is to the current menu item, not the full app-state. So i cannot access :location.
How is this handled?
I would prefer to aviod core.async and channels for the time being if possible.
Now that we have reference cursors you could probably do something like this:
(def app-state (atom {:location ""
:menuitems [["Pages" "/pages/"]
["Images" "/images/"]]}))
(defn location []
(om/ref-cursor (:location (om/root-cursor app-state))))
(defn menu-item-view [item owner]
(reify
om/IRender
(render [this]
(let [x (location)]
(dom/li #js {:className (if (= x (last item)) "active" "inactive")}
(dom/a #js
{:onClick (fn [_] (om/update! x (last #item)))}
(first item)))))))
(defn menu-view [app owner]
(reify
om/IRender
(render [this]
(dom/li #js {:className "has-dropdown not-click"}
(dom/a nil "Menu")
(apply dom/ul #js {:className "dropdown"}
(om/build-all menu-item-view (:menuitems app)))))))
(om/root menu-view app-state
{:target (. js/document (getElementById "menu"))})
It's just an idea - I haven't actually tested it.
Yes, all updates should occur through om/transact! or om/update!.
You could pass the main cursor to the controls state in :init-state or :state. This would give you access to it for update.
Alternatively, you could avoid using om/build-all and use build directly to pass multiple cursors to the control as specified here.
Simply call the following instead:
(map #(om/build menu-item-view {:main-cursor app :menu-cursor %}) (:menuitems app))
Related
(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.
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
I am attempting to learn Om, and have come across something I don't understand. I would expect this code
(defn search-page-view [app owner]
(reify
om/IRender
(render [_]
(dom/div #js {:id "search-block"}
"Test")
(dom/div #js {:id "results-block"}
"Test2"))))
(om/root
search-page-view app-state
{:target (. js/document (getElementById "app"))})
to result in this html:
<div id="app>
<div id="search-block">
Test
</div>
<div id="results-block">
Test2
</div>
</div>
However, it does not! The first div containing Test does not display. What am I misunderstanding?
Edit with the solution (pointed out by FakeRainBrigand):
Changing the code to
(defn search-page-view [app owner]
(reify
om/IRender
(render [_]
(dom/div nil
(dom/div #js {:id "search-block"}
"Test")
(dom/div #js {:id "results-block"}
"Test2")))))
results in the expected html.
As explained here and by FakeRainBrigand explained, your render function must return a single renderable.
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?
During writting reactjs tutorial in clojurescrtipt i've found that this-as macro compiles to
(function(){var t = this; return t;}
which always points to window inside react classes. Sometimes i can workaround this by js* this but not inside let or map cuz they are also compiled to functions.
How can i access react js this inside let form?
Situation on fiddle: http://jsfiddle.net/VkebS/57/
and a piece of tutorial FYI:
(def comment-list
(React/createClass
#js{:render
(fn [] (dom/div #js {:className "commentList"}
(let [d (this-as t (.. t -props -data))]
(map #(commnt #js {:author (:author %)} (:text %)) d))))}))
PS: i can use native array for data and native map function
(def comment-list
(React/createClass
#js{:render
(fn [] (dom/div #js {:className "commentList"}
(.map (.. (js* "this") -props -data) #(commnt #js {:author (:author %)} (:text %)))))}))
that works, but...
this-as works if you use it at start of render function:
(def commnt
(React/createClass
#js {:render
(fn []
(this-as this
(dom/div #js {:className "comment"}
(dom/h2 #js {:className "commentAuthor"}
(.. this -props -author))
(dom/span #js {:dangerouslySetInnerHTML
#js{:__html
(.makeHtml converter (.. (js* "this") -props -children toString))}}))))}))
see also: https://github.com/swannodette/om/blob/master/src/om/dom.cljs#L34