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.
Related
I've written a small experimental application in Fulcro "2.6.0-RC8". If you run it you will see that the writing underneath the picture of the kitten - "Description for the card" is red. This is good - things are working. However the two buttons below the picture of the kitten should not look the same, but look identical apart from the text. The "Press primary" button should have its text in white and its background should be red. What am I doing wrong?
(ns applets.reakit-test.core
(:require [fulcro.client :as fc]
[fulcro.incubator.ui.reakit :as rk]
[fulcro.client.primitives :as prim :refer [defsc]]
[fulcro-css.css-injection :as injection]
[fulcro.client.dom :as dom]))
(def red "#FF0000")
(defsc PrimaryButton [_ {:keys [text]}]
{:query [:text]
:initial-state (fn [p] {:text p})
:css [[:.primary
{:background-color red
:border "none"
:color "white"}]]}
(rk/button :.primary text))
(def primary-button-ui (prim/factory PrimaryButton))
(defsc Experiment [_ {:keys [but-primary]}]
{:query [:but-primary (prim/get-query PrimaryButton)]
:ident (fn [] [:experiment/by-id :singleton])
:initial-state {:but-primary (prim/get-initial-state PrimaryButton "Press primary")}
:css [[:.container
{:margin "10px"}]
[:.head
{:color "#222"
:font-weight "bold"}]
[:.copy
{:color red}]]}
(dom/div
(rk/card :.container
(rk/heading :.head {:as "h3"} "Card Heading")
(rk/image {:src "https://placekitten.com/180/300" :alt "Kitten" :width 180 :height 300})
(rk/paragraph :.copy "Description for the card")
(rk/shadow))
(dom/div
(rk/button "Ordinary button")
(primary-button-ui but-primary))))
(def experiment-ui (prim/factory Experiment))
(defsc ReakitTestRoot [this {:keys [application ui/react-key]}]
{:query [:ui/react-key {:application (prim/get-query Experiment)}]
:initial-state (fn [_] {:application (prim/get-initial-state Experiment nil)
:ui/react-key "initial"})}
(dom/div {:key react-key}
(injection/style-element {:component this
:react-key react-key})
(experiment-ui application)))
(defonce app (atom nil))
(defn mount []
(reset! app (fc/mount #app ReakitTestRoot "app")))
(defn ^:export init [_]
(reset! app (fc/new-fulcro-client
:reconciler-options {:render-mode :keyframe}))
(mount))
Your query in the component Experiment is wrong. It should be a join: [{:but-primary (prim/get-query PrimaryButton)}]
(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.
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.
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'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))