I am trying to get into ClojureScript and Om. There is a specific case which has me running in circles.
I have a component that is first rendered without a key.
(defn model-view [data owner]
(reify
om/IWillMount
(will-mount [_]
(om/transact! data [:stats] (fn [] {}))
(go
(let [response ((<! (api/get-stats (data :id))) :body)
stats (:stats response)]
(om/update! data [:stats] stats))))
om/IRender
(render [_]
(dom/div nil
(dom/h3 nil (data :title))
;; Here I want to use the :stats key in data that I
;; queried for in IWillMount, but its not present
;; the first time this model is rendered. It's only present
;; AFTER IWillMount has ran.
(om/build model-stats-view (data :stats)))))
The first time this component is called, the :stats key is simply not present in data. That's why I do an API call to get its stats. But React still calls the render function, thus the component crashes.
How can I set an initial state in this component that gives data an empty map called :stats, thus preventing trying to render nil in the (om/build model-stats-view) call?
I prefer to do all of my initialization in init-state, then access it in render-state. And I put a go-loop in my did-mount. When you update your init-state (i.e. :e-map) in the go-loop, it forces a call to render/re-render of the component. I use this in all of my components for inter-component/intra-component messaging. Just push something into a pub/sub channel and we are off to the races.
;To update my state I use a function:
(defn set-owner-state! [owner old-map-key old-map new-map]
(om/set-state! owner {old-map-key (merge old-map new-map)}))
om/IInitState
(init-state [_]
(println "queue->init-state")
{:e-map {:active-fsm nil}})
om/IDidMount
(did-mount [_]
(go-loop []
(let [[v _] (alts! [server-fsm-events dispatcher-events])
current-state (om/get-state owner)
e-map (:e-map current-state)]
; what goes in here is the logic to respond to a message
; in my case I have a match, it could be a cond or a set of
; if's.
(set-owner-state! owner :e-map e-map {:active-fsm :active :task-paths nil})
...
om/IRenderState
(render-state [_ {:keys [e-map]}]
(println "e-map:" e-map)
...
Related
I have a ClojureScript component:
(defn main-panel []
(def nodes (-> #(re-frame/subscribe [::subs/nodes])))
(defn list-nodes [node]
(prn (node :name)))
(reagent/create-class
{:component-did-mount
(fn []
(api-service/get-file-tree))
:reagent-render
(fn []
(doseq [node nodes]
(if (= (node :depth) 0)
(list-nodes node))))}))
which prints strings to the console.
But when I change the first function to:
(defn list-nodes [node]
[:<>
[:h1 (node :name)]])
I don't get any HTML that is rendered - no errors and the browser window stays blank.
How can I change this so it renders html?
Update
In response to the answer below I have changed doseq for for.
The issue is now that I am getting the error Uncaught Invariant Violation: Objects are not valid as a React child which is fine if you're working with React because you can debug, but with reagent, not so much. consider the following. This function:
(defn node [nodes]
(prn (nodes :name)))
when called by this function:
:reagent-render
(fn []
(for [nodes all-nodes]
(if (= (nodes :depth) 0)
(do
(nodeComponent/node nodes)
))))
prints 5 strings. But changing the called function to this:
(defn node [nodes]
[:h1 (nodes :name)])
causes this error: Uncaught Invariant Violation: Objects are not valid as a React child.
Is this problem somehow related to another problem I'm having?
For one, def and defn create global bindings. In order to create local bindings, use let or letfn:
(defn main-panel []
(let [nodes (-> #(re-frame/subscribe [::subs/nodes]))
(letfn [(list-nodes [node]
(prn (node :name)))]
(reagent/create-class
{:component-did-mount
(fn []
(api-service/get-file-tree))
:reagent-render
(fn []
(doseq [node nodes]
(if (= (node :depth) 0)
(list-nodes node))))}))
Your problem is that the render function should return the hiccup forms, but doseq returns nil. The function inside gets run (that's why you see console output from prn), but its return value is thrown away (that's why you see no HTML). An immediate fix might be to replace doseq with for, but
You could maybe do it like this (first reagent form):
(defn main-panel []
[:ul (for [node #(re-frame/subscribe [::subs/nodes])
:when (zero? (:depth node))]
^{:key (:id node)} ; whatever identifies nodes, for performance
[:li {:class "node"} (:name node)]])
I would trigger data fetching not here but wherever you startup your application (more or less parallel to the initial app render step).
I love re-frame, but I realize that I'm having a bit of trouble finding a nice pattern for handling AJAX responses.
My situation is the following:
I have a "global" event handler that triggers some AJAX call and dispatches to some other global event handler, depending on whether that call was successful, e.g.:
(reg-event-db :store-value
(fn [db [_ value]]
(do-ajax-store value
:on-success #(dispatch [:store-value-success %])
:on-error #(dispatch [:store-value-error %])
db))
(reg-event-db :store-value-success
(fn [db [_ result]]
(assoc db :foobar result)))
(reg-event-db :store-value-error
(fn [db [_ error]]
(assoc db :foobar nil
:last-error error)))
(I am aware of reg-event-fx and stuff, I'm just avoiding it here for the sake of brevity and because I think it does not make any difference for my problem).
I also have (multiple, distinct) UI components that might trigger the :store-value event, like so:
(defn button []
(let [processing? (reagent/atom false)]
(fn button-render []
[:button {:class (when #processing? "processing")
:on-click (fn []
(reset! processing? true)
(dispatch [:store-value 123]))}])))
So in this case the component has local state (processing?) that is supposed to depend on whether the AJAX call is still in progress or not.
Now, what is the proper pattern here to have the button component react to the events :store-value-success and :store-value-error in order to reset the processing? flag back to false after the AJAX call has finished?
Currently, I'm working around that problem by passing down callbacks but that seems really ugly because it clutters the event handlers' code with stuff that does not really belong there.
The best solution that I've thought of would be to have the button component being able to hook into the :store-value-success and :store-value-error events and install its own handler for those events, like this:
(defn button []
(let [processing? (reagent/atom false)]
(reg-event-db :store-value-success
(fn [db _]
(reset! processing? false)))
(reg-event-db :store-value-error
(fn [db _]
(reset! processing? false)))
(fn button-render []
[:button {:class (when #processing? "processing")
:on-click (fn []
(reset! processing? true)
(dispatch [:store-value 123]))}])))
However, that does not work. As it seems, re-frame does not allow multiple event handlers per event. Instead, a subsequent invocation of reg-event-db on one single event id will replace the previous event handler.
So how do you guys handle situations like this?
I think reg-event-fx (src) might indeed help solve your problem.
You could add a subscription that watches app-state e.g.
(rf/reg-sub
:app-state
(fn [db]
(get db :app-state)))
and add this to your button, perhaps with a state function e.g.
(defn btn-state [state]
(if (= state :processing)
"processing"))
And then in the AJAX handler, you could add an fx to update the state-
(reg-event-fx ;; -fx registration, not -db registration
:ajax-success
(fn [{:keys [db]} [_ result]]
{:db (assoc db :app-state :default)
:dispatch [:store-value-success result]}))
(reg-event-fx ;; -fx registration, not -db registration
:ajax-error
(fn [{:keys [db]} [_ result]]
{:db (assoc db :app-state :default)
:dispatch [:store-value-error result]}))
and update the AJAX handler
(reg-event-db :store-value
(fn [db [_ value]]
(do-ajax-store value
:on-success #(dispatch [:ajax-success %])
:on-error #(dispatch [:ajax-error %])
db))
This would be one way to handle it via -fx. I think you have already started to see the need for tracking app state, and I think bumping it up into the subscriptions would help with complexity, at which point your button render is greatly simplified.
(defn button []
[:button {:class (btn-state #app-state)
:on-click (dispatch [:store-value 123]))}])))
As others have mentioned, I would recommend to use http-fx and make processing? part of your global state. The code would look like this:
Events:
(reg-event-fx
:request
(fn [{:keys [db]} [_ method url data]]
{:http-xhrio {:method method
:uri url
:params data
:format (ajax/json-request-format)
:response-format (ajax/json-response-format {:keywords? true})
:on-success [:success-response method url]
:on-failure [:error-response method url]}
:db (assoc db :processing? true)}))
(reg-event-db
:success-response
(fn [db [_ method url result]]
(assoc db :foobar response
:processing? false)}))
(reg-event-db
:error
(fn [db [_ method url result]]
(assoc db :foobar nil
:last-error result
:processing? false)}))
Subscriptions:
(reg-sub
:processing
(fn [db _]
(:processing? db)))
View:
(defn button []
(let [processing? #(rf/subscribe [:processing])]
[:button {:class (when processing? "processing")
:on-click #(dispatch [:store-value 123]))}])))
Hint: You could reuse this code with all your requests.
I am trying to write a test like this:
(deftest login-form-rendering
(async done (with-redefs [http/post fake-login-success]
(with-mounted-component (c/login-form login!)
(fn [c div]
(.click (.getElementById js/document "Login"))
(is (= (:page #app-state) :location)))))
(done)))
I have this mock:
(defn fake-login-success [& _]
(let [ch (chan)
response {:status 200}]
(go (>! ch response)
ch)))
The login function does this:
(defn login! [username password]
(go (let [result (->> {:form-params {:username #username :password #password}}
(http/post "/api/login")
(<!))
{status :status body :body} result]
(case status
401 (swap! app-state assoc :page :bad-login)
200 (swap! app-state assoc :page :location)))))
The login form is a reagent component that takes a callback for onClick. app-state is a globally accessible atom.
The problem I'm facing is that the go block inside login! is never executed. Is there something I need to do to flush the channel or something?
I see there is also this unanswered question that's similar: Testing core.async code in ClojureScript. One difference seems to be that I don't have an explicit channel in my code under test. That's being generated by the cljs-http post.
I believe your problem is a misplaced parenthesis.
(let [ch (chan)
response {:status 200}]
(go (>! ch response)
ch))
The return value of this let is the go block's channel. It is a channel containing nothing, since:
The go block is trying to put on an unbuffered channel, requiring some other operation to "meet" it and do the complementary take in order for its own execution to proceed.
ch is lexically hidden from the outside world, so no operation can exist which can perform the complementary take.
If something somehow did take from it (or we added a buffer to ch so the first put succeeds), ch would be put on the returned channel.
(let [ch (chan)
response {:status 200}]
(go (>! ch response))
ch)
The return value of this let is ch, with no additional channel wrapping it. The go-block is parked trying to put on ch until something like your test gets around to taking from it.
However, since we're ultimately just looking to construct a channel with a constant on it, go itself seems like the simplest solution:
(defn fake-login-success [& _]
(go {:status 200})
I have one defresource, that is supposed to take POST-requests, validate request body in :malformed-decision, save the body to database in :post!-decision and return the saved body in :handle-created.
(defn parse-project [context] (json/read-str
(slurp (get-in context [:request :body]))
:key-fn keyword))
(defresource add-new-project
:malformed? (fn[ctx] (not (project-is-valid (parse-project ctx))))
:handle-malformed (fn [_] (generate-string (str "Malformed json!")))
...
:post! (fn [ctx] (save-to-db (parse-project ctx))
:handle-created (fn [ctx] (... parse-project ...))
So my code reads three times the ByteArrayInputStream(that comes from :request :body) with slurp-function. First time works but second time when slurp is called, nil is passed as a param and comes java.io.EOFException: JSON error. I think the reader starts reading where it was left last time.
How could I read the body of the request three times? Or is there nice way to save the result of reading to variable and pass that to other liberator-decisions?
The context can be updated by the outcome of each decision and action functions. You can parse the project once in malformed? and return a map with the parsed project which will be merged into the context so it is available to the following decisions and actions. For example:
(defresource add-new-project
:malformed? (fn[ctx] (let [project (parse-project ctx)]
(when (project-is-valid project)
{:project project})))
:handle-malformed (fn [_] (generate-string (str "Malformed json!")))
:post! (fn [ctx] (save-to-db (:project ctx)))
:handle-created (fn [ctx] (do-something (:project ctx))))
If the project is valid, :malformed? returns the {:project project} map which will be merged into the context to be used in the next decisions and actions.
If the project is not valid, it will return nil so execution continues in :handle-malformed.
For more info on liberator's execution model, see https://clojure-liberator.github.io/liberator/doc/execution-model.html
How to evaluate foo when mouse is down and moving using core/async?
Whilst attempting to learn the concepts behind core/async I have worked through the ClojureScript 101 tutorial (but I suspect this question applies to clojure to).
I create a channel where mouse movement events are placed using the following:
;; helper to get a channel where a dom event type will be put
(defn listen [el type]
(let [out (chan)]
(events/listen el type
(fn [e] (put! out e)))
out))
;; create a channel for mouse moves, take the values
;; and pass them to the console
(let [moves (listen (dom/getElement "canvas") "mousemove")]
(go (while true
(foo (<! moves)))))
This works, foo is evaluated when the mouse moves. But how can this be done only when the mouse is down?
My first guess would be to use an atom and two new channels for mousedown and mouseup. Then update atom with the mouse state and test against this in the go block. But I suspect this is wrong due to the use of an atom; hence the question.
Answering my own question, here is the closest I have got. Appears to work.
;; Util for create DOM channels
(defn listen [el type]
"Takes a DOM element and an event type. Returns a channel for the event"
;; out is a new channel
(let [out (chan (sliding-buffer 1))]
;; attach an event listener
(events/listen el type
;; the handler/callback of the listener takes the
;; event and put! in on the channel. We are using
;; put because we are not in a go block
(fn [e] (put! out e)))
;; return the channel
out))
(def canvas-el (dom/getElement "canvas"))
(def mouse-up (listen canvas-el "mouseup"))
(def mouse-down (listen canvas-el "mousedown"))
(def mouse-move (listen canvas-el "mousemove"))
(go (while true
(<! mouse-down)
(loop []
(let [[v ch] (alts! [mouse-move mouse-up])]
(when (= ch mouse-move)
(do
(.log js/console "move" (.-clientX v) (.-clientY v))
(recur)))))))