Why does Reagent render JSON in three ways? - json

I am trying to render JSON data from an API call in Clojurescript/Reagent. When I use js/alert I see the json I expect: ["Sue" "Bob"]
(defn- call-api [endpoint]
(go
(let [response (<! (http/get endpoint))]
(:names (:body response)))))
;; -------------------------
;; Views
(defn home-page []
[:div (call-api "/api/names")])
This is how I'm referencing the libraries (in case there's an problem there).
(ns myapp.core
(:require [reagent.core :as reagent :refer [atom]]
[reagent.session :as session]
[cljs-http.client :as http]
[cljs.core.async :refer [<! >!]]
[secretary.core :as secretary :include-macros true]
[accountant.core :as accountant])
(:require-macros [cljs.core.async.macros :refer [go]]))
But when I log it to the console, I get a long hash that looks nothing like the API response. The browser renders "00000000000120".
Why do these results differ? (browser, alert window, console message)
How can I get what I'm seeing in the alert window to render on the page?

When you call call-api it is going to return a go block. Instead of trying to consume that go block directly in your Reagent function, you could instead update the return value in a ratom.
(def app-state (atom)) ;; ratom
(defn- call-api [endpoint]
(go
(let [response (<! (http/get endpoint))]
(reset! app-state (:names (:body response))))))
(defn home-page []
[:div #app-state])
(defn main []
(call-api))

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

How do I extract the body from an HTTP request in Clojure?

I am making an HTTP request:
(defn main-panel []
(def API-URL "https://api.chucknorris.io/jokes/random")
(defn getFileTree []
(go (let [response (<! (http/get API-URL
{:with-credentials? false
:headers {"Content-Type" "application/json"}}))]
(:status response)
(js/console.log (:body response))))) ; prints a very complex data structure
(go
(let [result (<! (getFileTree))]
(.log js/console (:body result)))) ; prints null
:reagent-render
(fn []
[:h1 "kjdfkjndfkjn"]))
But I can't get to the "joke" in the returned object, array item 13:
How do I assign this value to a let or def?
Also, why does the second console.log print null?
Update
I am now moving on from using reagent atoms to reframe.
This is my component that successfully GETs data, updates the re-frame 'database':
(defn main-panel []
(def API-URL "https://api.chucknorris.io/jokes/random")
(def request-opts {:with-credentials? false})
(defn getFileTree []
(go (let [response (<! (http/get API-URL request-opts))]
(re-frame/dispatch [:update-quote response]))))
(defn render-quote []
(println (re-frame/subscribe [::subs/quote])) ;successfully prints API data as in screenshot below
(fn []
(let [quote-data (re-frame/subscribe [::subs/quote])
quote-text (if quote-data (:value quote-data) "...loading...")]
[:div
[:h3 "Chuck quote of the day"]
[:em quote-text]])))
(fn []
(getFileTree)
[render-quote]))
But this is the object I get back from the re-frame database:
As you can see it comes wrapped in the Reaction tags and I can't access the body or value any more. How do I access those?
I have a small working version using the reagent template. Create a new project (assuming you have Leiningen installed) with: lein new reagent chuck. This will create a project with many dependencies, but it works out of the box.
Next, edit the file at src/cljs/chuck/core.cljs and edit it so it looks like the following:
(ns chuck.core
(:require-macros [cljs.core.async.macros :refer [go]])
(:require [reagent.core :as reagent :refer [atom]]
[cljs-http.client :as http]
[cljs.core.async :refer [<!]]))
(def api-url "https://api.chucknorris.io/jokes/random")
(def request-opts {:with-credentials? false
:headers {"Content-Type" "application/json"}})
(def api-response (atom nil))
(defn get-quote []
(go
(let [response (<! (http/get api-url request-opts))]
(println response)
(reset! api-response response))))
(defn render-quote []
(fn []
(let [quote-data (:body #api-response)
quote-text (if quote-data (:value quote-data) "...loading...")]
[:div
[:h3 "Chuck quote of the day"]
[:em quote-text]])))
(defn quote-page []
(fn []
(do
(get-quote)
[:div
[:header
[render-quote]]
[:footer
[:p "footer here"]]])))
;; -------------------------
;; Initialize app
(defn mount-root []
(reagent/render [quote-page] (.getElementById js/document "app")))
(defn init! []
(mount-root))
I'll explain the relevant bits:
init will bootstrap the basics of the front-end, but in our case it's just calls mount-root which starts reagent telling it to call quote-page and placing the results in the DOM replacing the element with the ID of app.
quote-page calls get-quote which will call the API using the cljs-http library. I'm not checking for errors here, but basically when the request completes (either success or error) it will read the results from the channel (using <!) and place the response in response. The key is that response is a nested ClojureScript map that you can inspect to check if the result was successful or not. Note that I'm also printing the results with println instead of JS interop (.log js/console xxx) because console.log will show the inner details of how the nested map is implemented, which is not relevant for this case.
One the response is available, I store the results of the response in an atom called api-response. The key here is that the atom will contain nothing for a bit (while the request completes) and then the response will be inside it and reagent will take care of detecting the change and re-rendering.
Finally, quote-page calls render-quote which generates the markup for rendering the quote or a placeholder while it loads.
To run the whole thing, open a terminal and run lein run which will start a web server listening on port 3000 by default. In another terminal, run lein figwheel which will compile the ClojureScript code for you. One figwheel is ready it will start a REPL, and you can open the address http://0.0.0.0:3000/ in your computer to view the page.

Routing using default template in reagent

I am trying to use reagent to build my very basic project but I have a problem with routing and its parameter. This is from reagent looks like
EDITED - :require s added
(ns hammerslider.core
(:require [reagent.core :as reagent :refer [atom]]
[secretary.core :as secretary :include-macros true]
[accountant.core :as accountant]))
;; Views
(defn home-page []
[:div [:h2 "Welcome to hammerslider"]
[:div [:a {:href "/c/12"} "go custom"]]])
(defn c [test]
[:div [:h2 (str "on C " test)]
[:div [:a {:href "/"} "go to the home page"]]])
I am trying to get 12 from c route which is the route handling is look like this
(def page (atom #'home-page))
(defn current-page []
[:div [#page]])
(secretary/defroute "/" []
(reset! page #'home-page))
(secretary/defroute "/c/:test" [test]
(reset! page #'c)
I'm trying to catch the test parameter with the view function but it appears on C, not on C 12. How do I get to transfer the test parameter in to the view of c? or should I save it on different atoms?
EDITED - Mine solved by saving parameters into atom and it works, but is it the right way to pass the parameter?
(def parameter (atom ()))
(defn c []
[:div [:h2 (str "on C " (:test #parameter))]
[:div [:a {:href "/"} "go to the home page"]]])
(secretary/defroute "/c/:test" {:as params}
(do (js/console.log params)
(reset! parameter params)
(reset! page #'c)
))
It is depended on how you use your route parameters. The only guarantee between your program and reagent is if the value in ratom changed, the reagent component will be changed accordingly.
The TodoMVC is quite feature completed example for you to use reagent and secretary.
https://github.com/tastejs/todomvc/blob/gh-pages/examples/reagent/src/cljs/todomvc/routes.cljs
By the way, most of the time I will use re-frame instead of using reagent directly.

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.

transact app-state in will-mount has no effect

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