re-frame: reset atom after dispatch - clojurescript

I have this form:
(defn input-question
[]
(let [new-question (reagent/atom "")]
(fn []
[:div
[:input {:type "text"
:value #new-question
:on-change #(reset! new-question (-> % .-target .-value))}]
[:input {:type "button"
:value "Save new question"
:on-click #(re-frame.core/dispatch [:create-question #new-question])} ] ])))
How can I reset #new-question to "" (empty string) after the dispatch?

You can use reset! on the ratom after dispatching:
#(do (re-frame.core/dispatch [:create-question #new-question])
(reset! new-question ""))
to reset it after dispatching the value.

You probably want to review the re-frame effects docs:
https://github.com/Day8/re-frame/blob/master/docs/EffectfulHandlers.md
https://github.com/Day8/re-frame/blob/master/docs/Effects.md
Note that you can also use dispatch-n:
https://github.com/Day8/re-frame/blob/master/docs/API.md#dispatch-n
and you might want to use the fn syntax instead of the #(...) shorthand function syntax:
:input {:type "button"
:value "Save new question"
:on-click (fn []
(re-frame.core/dispatch [:create-question #new-question])
(reset! new-question "")) } ]

You can also use both events and subs to keep as much logic out of your view code as possible. This means you will end up with many any events and subs, however this is by design and idiomatic to re-frame. This makes your re-frame code easier to understand, decoupled and more testable. Here is an example:
(rf/reg-fx
:save-question
(fn [question]))
;; Handle creating a question
(rf/reg-sub
:new-question-value
(fn [db _]
(get-in db [:new-question :value])))
(rf/reg-event-db
:on-new-question-change
(fn [db [_ value]]
(assoc-in db [:new-question :value] value)))
(rf/reg-event-fx
:on-save-question-click
(fn [{:keys [db]} _]
{:db (assoc-in db [:new-question :value] "")
:save-question (get-in db [:new-question :value])}))
(defn input-question
[]
(let [new-question-value (rf/subscribe [:new-question-value])
on-save-question-click #(rf/dispatch [:on-save-question-click])
on-new-question-change #(rf/dispatch [:on-new-question-change (.. % -target -value)])]
(fn []
[:div
[:input {:type "text"
:value #new-question-value
:on-change on-new-question-change}]
[:input {:type "button"
:value "Save new question"
:on-click on-save-question-click}]])))
Some extra notes about this code:
You should namespace your events and subs keys to prevent naming clashes
You should define a function and pass that into reg-fx, reg-event-db, reg-event-fx & reg-sub. Doing this can make the code more testable by allowing test code to call the function handler directly. However you can still test using Day8/re-frame-test but it's a little harder.

Related

changing db value in input form in reframe

I've been trouble by an updating issue in reframe.
db.cljs:
{:apple 16}
event.cljs
(rf/reg-event-db
:update-apple-size
(fn [ db [_ size] ]
(assoc db :apple size)))
subs.cljs
(rf/reg-subs
::apple-size
(fn [ db ]
(get db :apple)))
views.cljs
[:input {:type :text
:on-change (rf/dispatch [:update-apple-size (.. % -target -value js/parseFloat))]
:value #(rf/subscribe [::apple-size] }]
The issue happed when user udpating with decimal number of the size. When user try to update current 16 to 16.5.
User will strike . ,but this will be trigger on-change event and wont' update the db value. ( {:apple 16} => {:apple 16.0} ) ?. Then the subscribe will render "16.0" to UI as "16" ?
that's why user can't really append decimals after an integer in input element ?
one way I can think of is stringyfy all values in db but this will create lots of trouble since there are lots of calculation assuming the values are number in the db.
Any advise will be helpful !
Note that js/parseFloat parses a string with a decimal dot into an integer:
cljs.user=> (js/parseFloat "16.")
16
One solution can be to save into db two values:
the value of input as a string
the parsed value of input
Then input will display a string with a decimal dot. See example:
Db:
(def default-db
{:input-val "5"
:input-num 5})
Views:
[:input {:type "string"
:value #(re-frame/subscribe [::subs/input-val])
:on-change #(re-frame.core/dispatch [:change-val (-> % .-target .-value)])}]
[:div
[:p #(re-frame/subscribe [::subs/input-num])]]
Subs:
(re-frame/reg-sub
::input-val
(fn [db] (:input-val db)))
(re-frame/reg-sub
::input-num
(fn [db] (:input-num db)))
Events:
(re-frame/reg-event-db
:change-val
(fn [db [_ val]]
(-> db
(assoc :input-val val)
(assoc :input-num (js/parseFloat val)))))

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

Reading Input With Om Next

I'm trying to understand how to read state from a text box in om.next. As I understand it, we are no longer bound/supposed to use core.async.
As a small example, consider writing in a textbox and binding it to a paragraph element, so that the text you enter automatically appears on the screen.
(def app-state (atom {:input-text "starting text"}))
(defn read-fn
[{:keys [state] :as env} key params]
(let [st #state]
(if-let [[_ v] (find st key)]
{:value v}
{:value :not-found})))
(defn mutate-fn
[{:keys [state] :as env} key {:keys [mytext]}]
(if (= 'update-text key)
{:value {:keys [:input-text]}
:action
(fn []
(swap! state assoc :input-text mytext))}
{:value :not-found}))
(defui RootView
static om/IQuery
(query [_]
[:input-text])
Object
(render [_]
(let [{:keys [input-text]} (om/props _)]
(dom/div nil
(dom/input
#js {:id "mybox"
:type "text"
:value input-text
:onChange #(om/transact! _ '[(update-text {:mytext (.-value (gdom/getElement "mybox"))})])
})
(dom/p nil input-text)))))
This doesn't work.
When firing the onChange event in the input form, the quoted expression does not grab the text from the box.
The first mutation fires and updates, but then subsequent mutations are not fired. Even though the state doesn't changed, should the query read the string from app-state and force the text to be the same?
I would make the :onChange event look like this:
:onChange (fn (_)
(let [v (.-value (gdom/getElement "mybox"))]
#(om/transact! this `[(update-text {:mytext ~v})])))
Here the value v will actually be going through. But also om/transact! needs either a component or the reconciler to be passed as its first parameter. Here I'm passing in this which will be the root component.