Routing using default template in reagent - clojurescript

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.

Related

Reagent Component not Re-Rendering on Prop Change

My Reagent component ist a simple div that has a component-did-mount and a component-did-update hook. It draws notes using vexflow.
(defn note-bar [notes]
(reagent/create-class
{:display-name "Note Bar"
:reagent-render (fn [notes]
^{:key notes} ;; force update
[:div#note-bar])
:component-did-mount (fn [this]
(draw-system-with-chord notes))
:component-did-update (fn [this]
(draw-system-with-chord notes))}))
It is used like this.
(defn exercise-one []
(let [note (re-frame/subscribe [:exercise-one/note])]
[:div
[note-bar/note-bar #note]
[other]
[components]]))
My event code is the following.
(defn store-exercise-one-note [db [_ note]]
(assoc-in db [:exercise-one :note-bar :note] note))
(re-frame/reg-event-db
:exercise-one/store-note
store-exercise-one-note)
(defn query-exercise-one-note [db]
(or (get-in db [:exercise-one :note-bar :note])
[{:octave 4 :key :c}]))
(re-frame/reg-sub
:exercise-one/note
query-exercise-one-note)
I verified that the app-db value changes using 10x. Yet the note bar only displays a different note when Hot Reloading kicks in. I believe this is due to the component-did-update hook not being called.
My question is, is this the right way to bind a JavaScript library that renders something? If so, why does my component not update?
The following fixed the component. See the documentation about form-3 components here
(defn note-bar [notes]
(reagent/create-class
{:display-name "Note Bar"
:reagent-render (fn [notes]
^{:key notes} ;; force update
[:div#note-bar])
:component-did-mount (fn []
(draw-system-with-chord notes))
:component-did-update (fn [this]
(let [new-notes (rest (reagent/argv this))]
(apply draw-system-with-chord new-notes)))}))

Problem opening files with the FileReader API

This has been bugging me for days. I have a web app that lets the user open documents from their local machine. I'm using the FileReader API for the first time.
It works correctly except for one use case.
Open a document file.
Programmatically create a new document, overwriting the existing one.
Open the same file as above.
When this sequence is executed, the second attempt fails silently (except that the file is not loaded).
Here is an example Reagent program (created from the figwheel-main template) that illustrates the problem.
(ns a-bad-button.core
(:require [reagent.core :as r]))
(def app-state-ratom (r/atom nil))
(defn new-doc []
{:doc-text "Some MINIMAL text to play with."})
(defn add-new-button
[aps]
(fn [aps]
[:input.tree-demo--button
{:type "button"
:value "New"
:on-click #(reset! aps (new-doc))}]))
(defn load-doc-data!
[aps file-data]
(swap! aps assoc :doc-text file-data))
(defn handle-file-open-selection
[aps evt]
(let [js-file-reader (js/FileReader.)]
(set! (.-onload js-file-reader)
(fn [evt] (load-doc-data! aps (-> evt .-target .-result))))
(.readAsText js-file-reader (aget (.-files (.-target evt)) 0))))
(defn add-open-button
[aps]
(fn [aps]
[:div
[:input {:type "file" :id "file-open-id"
:style {:display "none"}
:on-change #(handle-file-open-selection aps %)}]
[:input {:type "button"
:value "Open"
:on-click #(.click (.getElementById js/document "file-open-id"))}]]))
(defn a-bad-button
[aps]
(fn [aps]
[:div
[:h4 "A Bad Button"]
[:p#doc-text-p (or (:doc-text #aps) "Loaded text will go here.")]
[add-new-button aps]
[add-open-button aps]]))
(defn mount! [el]
(reset! app-state-ratom (new-doc))
(r/render-component [a-bad-button app-state-ratom] el))
(defn mount-app-element []
(when-let [el (.getElementById js/document "app")]
(mount! el)))
(mount-app-element)
(defn ^:after-load on-reload []
(mount-app-element))
With println debugging messages, it appears that execution reaches the :on-click handler in the add-open-button function, but the handler, handle-file-open-selection, is never reached or executed.
The failure occurs on Safari, Opera, Brave, and Vivaldi browsers. Files open as expected on Firefox.
Has anyone seen this before and fixed it?
Similar questions:
Filereader - upload same file again not working
FileReader onload not getting fired when selecting same file in Chrome
Basically, the problem is that onChange will not trigger when selecting the same file. One workaround is to set the value of the file input before the file browser opens to something like "", to always trigger an onChange event. In your case, it could look like changing your handle-file-open-selection function to:
(defn handle-file-open-selection
[aps evt]
(let [js-file-reader (js/FileReader.)]
(set! (.-onload js-file-reader)
(fn [evt]
(load-doc-data! aps (-> evt .-target .-result))))
(.readAsText js-file-reader (aget (.-files (.-target evt)) 0))
;; add this
(set! (.-value (.getElementById js/document "file-open-id")) "")
))

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.

How to define default component in Reagent

I am trying to create SPA application with Reagent. How I can define default page/component? As I understand it, I should save current page in state atom. But I can't define it if I change state in home page.
For example, in this code home cannot be resolved (row 1):
(defonce app-state (atom {:current-page (home)}))
(defn second-page []
[:p 2])
(defn home []
[:div
[:p 1]
[:input {:type "button" :value "Click!"
:on-click #(swap! app-state second-page)}]])
(defn hello-world []
[:div
[:input {:type "button" :value "Home" :on-click #(swap! app-state home)}]
(:current-page #app-state)
[:h3 "footer"]])
(reagent/render-component [hello-world]
(. js/document (getElementById "app")))
There are a few ways you can solve this issue. One approach would be to create a map called pages like so
(def pages {:home home
:second second-page
....}
and then use the key name in the state atom i.e.
(defonce app-state (atom {:current-page :home})
Because you are just storing a keyword, there is no need for forward delarations as keywords evaluate to themselves. Then in your code, you would use the value for :current-page to lookup the compoonent from the pages map
defn hello-world []
[:div
[:input {:type "button" :value "Home" :on-click #(swap! app-state home)}]
((:current-page #app-state) pages)
[:h3 "footer"]])
You may also find things like secretary useful as an example of one way to generalise this sort of idea.
Just use declare to put placeholders for home and second-page before the atom:
(declare home second-page)
(defonce app-state (atom {:current-page home}))
Be sure to always keep a browser tab open to The Clojure CheatSheet. See also this answer for more details on the workings of Clojure's var feature.

Why does Reagent render JSON in three ways?

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