transact app-state in will-mount has no effect - clojurescript

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

Related

Layout component unable to render when Reitit router introduced

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

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

Syntax for giving a button a particular width

How do you set a button to a particular width? This is one of the things I have tried so far:
(:require [om.next :as om :refer-macros [defui]]
[om.dom :as dom])
(defui HelloWorld
Object
(render [this]
(dom/button #js {:style {:width 300}} (get (om/props this) :title))))
Setting the title of the button works fine and is probably not relevant for this question. I've left it in because it is a typical thing to be doing, and placement of the attributes might be important.
The lein project.clj file has these 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 think the problem is due to #js only working on the top level. #JS will work on a top level map {} or vector [], but if you have nested data as values, you need to include additional #js calls for each embedded object.
What you really need is
(:require [om.next :as om :refer-macros [defui]]
[om.dom :as dom])
(defui HelloWorld
Object
(render [this]
(dom/button #js {:style #js {:width 300}} (get (om/props this) :title))))
Have a look at this post on using #js. For readability, rather than nested #js calls, you are often better off using clj->js
I got it to work with this:
(defui HelloWorld
Object
(render [this]
(dom/button (clj->js {:style {:width 300}}) (get (om/props this) :title))))
Note the use of clj->js.

Om ref cursor not re-rendering components when updated

(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.

How to create Material UI component in Om Clojurescript?

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.