How to pass new props to state of acomponent in Reagent? - clojure

I have a component:
(defn inner-input [cljs_element activeEl title]
(let [form (atom title)]
(fn [cljs_element activeEl title]
[:input {:type "text"
:onChange #(reset! form (.. % -target -value))
:on-blur #(change-title cljs_element (.. % -target -value))
:style {:display (if (:active (:node cljs_element)) "block" "none")
:width (* (+ 1 (count #form)) 8)
:max-width 730
:min-width 170}
:value #form}])))
It is nested in other component:
(defn card-input [cljs_element activeEl]
(fn [cljs_element activeEl]
(let [title (:title (:node cljs_element))]
[:div
[inner-input cljs_element activeEl title]])))
When i type data to the input in the inner-input component, i need update the local state form. And when the outer component card-input updates i want to reset my form to new title prop from argument. How can i achieve that?
I tried put (reset! form title) between let and fn in the inner-input component but it will not help

You can use reagent/track! to listen to changes to title, and reagent/dispose to stop listening. You can alternatively use add-watch and remove-watch, but track is a more convenient syntax.
(defn inner-input [title]
(reagent/with-let
[form (reagent/atom #title)
watch (reagent/track! (fn [] (reset! form #title)))]
[:label
"Inner input"
[:input {:on-change (fn [e]
(reset! form (.. e -target -value)))
:on-blur (fn [e]
(reset! title (.. e -target -value)))
:value #form}]]
(finally
(reagent/dispose! watch))))
(defn card-input []
(reagent/with-let
[title (reagent/atom "hello")]
[:div
[:label "Title"
[:input {:on-change (fn [e]
(reset! title (.. e -target -value)))
:value #title}]]
[inner-input title]]))
Now if you type in the inner input it will only update the outer when you exit the input box, but changing the outer title will immediately change the inner one. Is that what you wanted?
But if you don't want to pass title as a ratom and have to pass it as a value, then instead you can compare it to the previous value to determine if it changed, and reset form only when it changes.
(when (not= #previous-title title)
(do (reset! previous-title title)
(reset! form title)))
This code can go in render seeing as it is safe to call when form changes... nothing will happen.

Related

Having problems with reagent atom and resert! function

Hi guys I'm new at Clojure and ClojureScript...
I'm filling out 2 select element ... What I wonna do is update the second select depending on what option the user choose in the first select.
This is my code:
(ns easy-recipe.components.calculate
(:require [reagent.core :as r :refer [atom]]
[ajax.core :as ajax]
[reagent.session :as session]
[easy-recipe.components.common :refer [input select button]]
[easy-recipe.bulma.core :refer [horizontal-select horizontal-input-has-addons table columns box]]))
(defn handler [request]
(session/put! :categories (get-in request [:body :categories]))
(session/put! :breads (get-in request [:body :breads]))
(session/put! :recipes (get-in request [:body :recipes])))
(defn error-hadler [request]
(let [errors {:server-error (get-in request [:body :erros])}]
errors))
(defn get-categories-breads-recipes []
(ajax/GET "/api/calculate"
{:handler handler
:error-hadler error-hadler}))
(defn select-fun-wrapper [breads]
(fn [e]
(let [category_value (-> e .-target .-value)]
(reset! breads
(filter
#(= category_value (str (% :category_id)))
(session/get :breads))))))
(defn calculate-page []
(get-categories-breads-recipes)
(let [fields (atom {})
categories (atom nil)
breads (atom nil)
recipe (atom nil)]
(fn []
(reset! categories (session/get :categories))
;(reset! breads (filter #(= 1 (% :category_id)) (session/get :breads)))
[:div.container
[box
[horizontal-select "Category"
[select "category" "select" #categories (select-fun-wrapper breads)]]
[horizontal-select "Bread"
[select "bread" "select" #breads #()]]
[horizontal-input-has-addons "Quantity"
[input "quantity" "input" :text "" fields]
[button "quantity-btn" "button" "Add" #() nil]]]
[box
[columns
[table ["Ingredients" "Quantity"] #recipe]
[table ["Product" " "] [["30 Panes" "x"]]]]]])))
As you have noticed the breads reset! is commented... now the "select-fun-wrapper" is working fine, it updates the bread select depening on the category option selected... but if I uncomment that line the "select-fun-wrapper" will stop working (not updating the second select)... I wonna know why does it happend?
I can not leave this line commented because right now I have the problem thar the "Bread" select starts empthy... How could I fill the bread atom without using the reset! function?
Extra code (if it makes it clear):
(ns easy-recipe.bulma.core)
(defn horizontal-select [label select]
[:div.field.is-horizontal
[:div.field-label.is-normal>label.label label]
[:div.field-body>div.field.is-narrow>div.control>div.select.is-fullwidth
select]])
....
(ns easy-recipe.components.common)
(defn select [id class options function]
[:select {:id id :class class :on-change function}
(for [opt options]
^{:key (opt :id)}
[:option {:value (opt :id)} (opt :name)])])
....

How to get files from <input type='file' …/> in ClojureScript

I'm trying to get image file in input field but I could not do it. Here is the code:
:on-change (fn [_]
(this-as this
(println "Files: " (.-files this))))
But (.-files this) returns nil.
Any ideas?
P.S: I would like to upload this image to my server.
Here is a workable snippet from our project:
:on-change
(fn [this]
(if (not (= "" (-> this .-target .-value)))
(let [^js/File file (-> this .-target .-files (aget 0))]
;; your logic here ...
;; now reset the widget to let user upload a new one
(set! (-> this .-target .-value) ""))))
Hope this would help.
To expand on Albert's answer (and because my edits were rejected):
Instead of using goog.object/get as below (and in his answer with o/get):
(let [dom (goog.object/get event "target")
file (goog.object/getValueByKeys dom #js ["files" 0])]))
You can also use regular interop forms in ClojureScript:
:on-change (fn [event]
(let [files (.. event -target -files) ; returns JS Array
file (first files)]
(do-something-with file)]))
you must first get the dom node from somewhere. In React world this might not be the dom since they are syntactic events.
:on-change (fn [event]
(let [dom (goog.object/get event "target")
file (goog.object/getValueByKeys dom #js ["files" 0])]))
Or using regular interop forms in ClojureScript:
:on-change (fn [event]
(let [files (.. event -target -files) ; returns JS Array
file (first files)]
(do-something-with file)]))
https://reactjs.org/docs/handling-events.html

How should one handle AJAX success/error responses in Clojure re-frame?

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.

reagent forms multi-select not working for list

I was following the sample code from http://yogthos.github.io/reagent-forms-example.html and was attempting the use the multi-select option for a list.
(defn select-item [item]
(go
(reset! current-selection item)
(let [response (<! (check-for-response))]
(reset! current-response response)
(reset! past-response response))))
;;batch
(defn item-list []
[:div#items-list
[items-list-header]
[:ul.list-group.items {:field :multi-select :id :pick-a-few}
(if (pos? (count #items))
(doall (for [item #items]
^{:key (item "upc")}
[:li.list-group-item [:a {:class (set-item-class item) :on-click #(select-item item) :href "#"}
(item "description")]]))
[:li [:a "No Items For This Department"]])]])
(defn product-component []
[:div
[item-list]
[product-response]
;[bind-fields item-list items]
;[bind-fields item-list product-response]
])
Does anyone know why I am unable to multi-select? The logic in select-item will change, but I can't seem to see the multi-select in the UI
I've been messing around with bind-fields in my product component with no success.

clojurescript + reagent issue

I am working on a simple web-app using clojurescript and reagent. I would like to create a simple "tab" component, which will contain (for starters) a text-input component.
The app has 2 tabs and the user has the option to choose a tab and I want to "preserve" the values in each of these two tabs.
Here's the code:
(defn atom-input [value]
[:input {:type "text"
:value #value
:on-change #(reset! value (-> % .-target .-value))}])
(defn simple-tab [index]
(let [pg-index (atom 1)
a (atom 0)]
(fn []
[:div
[:h4 (str "index: " #index)]
[atom-input a]])))
(defn main-page []
(let [index (atom 0)]
[:div.container
[:div.row
[:button {:on-click (fn [] (reset! index 0))} "select tab 1"]
[:button {:on-click (fn [] (reset! index 1))} "select tab 2"]]
[:div.row
[simple-tab index]]]))
(defn ^:export run []
(reagent/render-component
(fn [] [main-page])
(.-body js/document)))
The problem is that when I switch the tab, the components share the values of the input field - what am I please doing wrong here?
Thank you so much for your help!
The problem is you're passing a (atom 0) to the atom-input control: [atom-input a].
This caused the same atom value to be shared between your tabs.
If you don't want to share the value, you'll need change a to a map: a (atom {}) and pass the map and the index to atom-input, e.g.:
(defn atom-input [value index]
[:input {:type "text"
:value (or (get #value index) "")
:on-change #(swap! value assoc index (-> % .-target .-value))}])
(defn simple-tab [index]
(let [pg-index (atom 1)
a (atom {})]
(fn []
[:div
[:h4 (str "index: " #index)]
[atom-input a #index]])))
A better approach, IMHO, is to use cursor so you don't need to pass the index & the whole map to atom-input, e.g.:
(defn atom-input [value]
[:input {:type "text"
:value (or #value "")
:on-change #(reset! value (-> % .-target .-value))}])
(defn simple-tab [index]
(let [pg-index (atom 1)
a (atom {})]
(fn []
[:div
[:h4 (str "index: " #index)]
[atom-input (reagent/cursor [#index] a)]])))
I think there are a couple of problems here because you are mixing up application data (state) and display logic data (i.e. DOM). If you keep the two things distinct i.e. maintain your application state in one atom and data relating to component display in another, then things may be a bit cleaner.
Your simple-tab component does not need to know anything about the tab state. It just needs to know about the app state i.e. the value entered/stored via atom-input. Therefore, rather than passing it index, pass it the atom you want it to use. this would require some higher level logic to determine the call. for example, if you had a number of tabs, you might have something like
(condp = #index
0 [simple-tab tab0-atom]
1 [simple-tab tab1-atom]
...
n [simple-tab tabn-atom])
or you could modify simple-tab so that the value passed in i.e. index value is used as a key into the app-state - a cursor would be easiest I think i.e.
(def app-state (r/atom {:tabs {0 nil 1 nil}}})
(defn simple-tab [index]
(let [val-cur (r/cursor app-state [:tabs index])]
[atom-input val-cur]))
You are using a form-2 component, that is, a component that returns a function.
More details here : https://github.com/Day8/re-frame/wiki/Creating-Reagent-Components#form-2--a-function-returning-a-function
When doing so, only the returned function is called so your atom-inputs are sharing the same atom.
Also, you should use the same argument in the inner function
(defn simple-tab [index]
(let [pg-index (atom 1)
a (atom {})]
(fn [index]
[:div
[:h4 (str "index: " #index)]
[atom-input a #index]])))
In your case, you are passing an atom so this is not important but you could have error in the future if you forgot this.
Concerning the broader architecture, I would advise you to use a single global atom. Try to have as much as possible state in this atom and avoid component local state so this is easier to reason about.
You could also name your tabs like :product :users and use a multimethod to render the correct tab based on the selected one. This is easier to read and easier to add new tabs in the future.