I am trying to use ReactBootstrap's Modal component in a Reagent application but get a "Cannot read property 'findDOMNode' of undefined" error when attempting to show the modal.
I have [cljsjs/react-bootstrap "0.28.1-1"] as a dependency in my project.
Here is the source that I used to test with:
(ns react-bootstrap-test.core
(:require
[reagent.core :as r]
cljsjs.react-bootstrap))
(def rbutton (r/adapt-react-class js/ReactBootstrap.Button))
(def rmodal (r/adapt-react-class js/ReactBootstrap.Modal))
(def modal-header (r/adapt-react-class js/ReactBootstrap.Modal.Header))
(def modal-title (r/adapt-react-class js/ReactBootstrap.Modal.Title))
(def modal-body (r/adapt-react-class js/ReactBootstrap.Modal.Body))
(defonce show-modal (r/atom false))
(defn modal-test []
[:div
[rbutton {:bsStyle "primary"
:bsSize "large"
:active true
:on-click (fn [e]
(reset! show-modal true)
(.stopPropagation e))}
"Show Modal"]
[:div.rmodal
[rmodal {:show #show-modal}
[modal-header
[modal-title "Title"]]
[modal-body
[:h3 "Body"]]]]])
(r/render-component [modal-test]
(. js/document (getElementById "app")))
Related
I'm having trouble sorting out why a layout/parent component (app.components.layout) is not resolving after a hard reload BUT is okay after a hot reload with dev server after I introduced routes.
The error: TypeError: dapp.components.layout.render is not a function
This error doesn't appear when I render the view component in the #match atom directly.
(ns app.views
(:require
[reagent.core :as r]
[reitit.frontend :as rf]
[reitit.coercion.schema :as rsc]
[dapp.components.layout :as layout]
[dapp.components.dashboard :as dashboard]
[dapp.components.settings :as settings])))
(def routes
(rf/router
["/"
[""
{:name ::dashboard
:view dashboard/render
:controllers [{:start (log-fn "start" "dashboad controller")
:stop (log-fn "stop" "dashboard controller")}]}]
["settings"
{:name ::settings
:view settings/render}]]
{:data {:controllers [{:start (log-fn "start" "root-controller")
:stop (log-fn "stop" "root controller")}]
:coercion rsc/coercion}}))
;; broken on a hard browser refresh but works when shadow does a hot reload.
(defn main-panel []
[:div
(if #match
(let [view (:view (:data #match))]
(layout/render view #match)))]) ;; TypeError: app.components.layout.render is not a function
;; working
(defn main-panel []
[:div
(if #match
(let [view (:view (:data #match))]
(view nil))])
Layout Component:
(ns app.components.layout)
(defn render [view match]
[:div
[view]])
View Component:
(ns app.components.dashboard)
(defn render [props]
[:div
"Dashboard"])
Setup:
(ns app.core
(:require
[reagent.dom :as rdom]
[re-frame.core :as re-frame]
[reitit.frontend.easy :as rfe]
[reitit.frontend.controllers :as rfc]
[dapp.events :as events]
[dapp.views :as views]
[dapp.config :as config]
))
(defn dev-setup []
(when config/debug?
(println "dev mode")))
(defn ^:dev/after-load mount-root []
(re-frame/clear-subscription-cache!)
(let [root-el (.getElementById js/document "app")]
(rdom/unmount-component-at-node root-el)
(rdom/render [views/main-panel] root-el)))
(defn init []
(re-frame/dispatch-sync [::events/initialize-db])
(dev-setup)
;; router setup
(rfe/start!
views/routes
(fn [new-match]
(swap! views/match (fn [old-match]
(if new-match
(assoc new-match :controllers (rfc/apply-controllers (:controllers old-match) new-match))))))
{:use-fragment true})
(mount-root))
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