Correctly manipulating state in reagent - clojure

I just learn Reagent in Clojurescript, I am just following some tutorial but maybe I miss something I have this code for the state
(defonce app-state (atom {:text "Hello Chestnut!" :click-count 0}))
and the rendered view
(defn article []
[:div
[:div "The atom" [:code "click-count"] " has value : " (:click-count #app-state)]
[:input {:type "button" :value "Add"
:on-click #(swap! (:click-count #app-state) inc)}]
]
)
I'm trying to increment the state when they button is pressed, but I got this error on the console
Error: No protocol method ISwap.-swap! defined for type number: 0

the atom should be swapped not the :click-count
(swap! app-state update :click-count inc)

Related

Cljfx: two equal map's keys

I want to get this behavior: when the button is pressed with user the button's text and the label's text should are changed together.
But the problem is button and label have the equal name of keys for the text properties. And I can't store equal keys in one hash-map.
;;I have two atoms keeps the state of button text and state of label text
(def *button-text (atom
{:text "click me"}))
(def *label-text (atom
{:text "press the button"}))
;;I have the root function which should accepts arguments for button and label props.
But these props have equal names - text - and I can't store two equal keys in one map.
{:fx/type root
:text (:text *button-text)
:text (:text *label-text)}
;;This will cause an error.
This is how I solved this problem. But is too much of the code and out of normal way.
(ns examp.core
(:gen-class)
(:require [cljfx.api :as fx])
(:import [javafx.application Platform]))
(def *button-text (atom
{:text "click me"}))
(def *label-text (atom
{:text "press the button"}))
(def renderer (fx/create-renderer))
(defn root [{:keys [one two]}]
(let [button-text (:text one)
label-text (:text two)]
{:fx/type :stage
:showing true
:title "Window"
:width 250
:height 150
:scene {:fx/type :scene
:root {:fx/type :v-box
:alignment :center
:spacing 10
:children [{:fx/type :label
:text label-text}
{:fx/type :button
:min-width 100
:min-height 50
:text button-text
:on-action (fn [_]
(if (= button-text "click me")
(do
(swap! *button-text assoc :text "clicked")
(swap! *label-text assoc :text "button is pressed")
(renderer
{:fx/type root
:one #*button-text
:two #*label-text}))
(do
(swap! *button-text assoc :text "click me")
(swap! *label-text assoc :text "presse the button")
(renderer
{:fx/type root
:one #*button-text
:two #*label-text}))))}]}}}))
(defn -main [& args]
(Platform/setImplicitExit true)
(renderer {:fx/type root
:one #*button-text
:two #*label-text}))
Before I will show you my attempt, here is the link to the official Cljfx examples repository. These examples should be useful for you, as they show practices for managing app state, event handling and so on.
For this situation, I recommend studying e05_fn_fx_like_state.clj- this is also an example I based my code on:
(ns examp.core
(:require [cljfx.api :as fx])
(:gen-class))
(def *state
(atom {:label-text "Click the button."
:button-text "Click me!"}))
(defn root [{:keys [label-text button-text]}]
{:fx/type :stage
:showing true
:title "Window"
:width 250
:height 150
:scene {:fx/type :scene
:root {:fx/type :v-box
:alignment :center
:spacing 10
:children [{:fx/type :label
:text label-text}
{:fx/type :button
:text button-text
:min-width 100
:min-height 50
:on-action {:key :button-action}}]}}})
(defn map-event-handler [event]
(when (= (:key event) :button-action)
(if (= (:button-text #*state) "Click me!")
(reset! *state {:label-text "The button was clicked!"
:button-text "Clicked!"})
(reset! *state {:label-text "Click the button."
:button-text "Click me!"}))))
(def renderer
(fx/create-renderer
:middleware (fx/wrap-map-desc root)
:opts {:fx.opt/map-event-handler map-event-handler}))
(defn -main [& args]
(fx/mount-renderer *state renderer))

Simple Clojurescript form

I'm working with Reagent and CLJS, familiar with React and Clojure, less so CLJS. I'd like to make a simple form, but it's not obvious to me in CLJS.
(defn form []
[:div
[:input {:type "text" :name "first-name" :id "first-name"}]
[:button {:on-click (fn [e] (test-func "hello"))}
"Click me!"]
])
I want to grab the value of that input, and pass it to a function when the button is clicked. How do I get that input's value into my on-click function?
The idiomatic and technically correct way is to avoid keeping any state in DOM and accessing it directly. You shouldn't rely on the input's value. Keep the state as Reagent's atom. Then you can do anything with it.
(def first-name (r/atom ""))
(defn form []
[:div
[:input {:type "text"
:value #first-name
:on-change #(reset! first-name (.-value (.-target %)))
}]
[:button {:on-click #(test-func #first-name)} "Click me!"]])
You can grab the element's value like this: (.-value (.getElementById js/document "first-name"))
(defn form []
[:div
[:input {:type "text" :name "first-name" :id "first-name"}]
[:button {:on-click (fn [e] (test-func (.-value (.getElementById js/document "first-name"))))}
"Click me!"]
])
If there is a better answer out there, please share. :)

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

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.

Making a simple countdown timer with Clojure/Reagent

I am experimenting with Clojure and Reagent with almost no experience, trying to make a simple timer.
(defn reset-component [t]
[:input {:type "button" :value "Reset"
:on-click #(reset! t 60)}])
(defn countdown-component []
(let [seconds-left (atom 60)]
(fn []
(js/setTimeout #(swap! seconds-left dec) 1000)
[:div.timer
[:div "Time Remaining: " (show-time #seconds-left)]
[reset-component seconds-left]])))
The timer countdown appears to work correctly until I hit the reset button. After that, the timer starts counting down twice as fast. Every time I hit the reset button it counts down faster.
How can I get the timer to automatically count down when the page is loaded but not count down faster when the reset button is clicked?
Dereferencing seconds-left in reset-component triggers a rerender of the countdown component which attaches another decrementer function to your countdown-component.
For future reference, reagent 0.6.0+ comes with a new with-let macro, very useful for things that need proper "destruction", such as the setInterval/clearInterval:
(defn countdown-component []
(r/with-let [seconds-left (r/atom 60)
timer-fn (js/setInterval #(swap! seconds-left dec) 1000)]
[:div.timer
[:div "Time Remaining: " (str #seconds-left)]]
(finally (js/clearInterval timer-fn))))
Here's the official anouncement of the feature.
Note that calling setTimeout from the render function also works, but it is not seen as good practice. (e.g. https://clojurians.slack.com/archives/C0620C0C8/p1495568238109060)
The solution I found was to use setInterval before the component function rather than setTimeout inside the component function:
(defn reset-component [seconds]
[:input {:type "button" :value "Reset"
:on-click #(reset! seconds 60)}])
(defn countdown-component []
(let [seconds-left (atom 60)]
(js/setInterval #(swap! seconds-left dec) 1000)
(fn []
[:div.timer
[:div "Time Remaining: " #seconds-left]
[reset-component seconds-left]])))
You probably want this:
(defn reset-component [t]
[:input {:type "button" :value "Reset"
:on-click #(reset! t 60)}])
(defn countdown-component []
(let [seconds-left (atom 60)]
(js/setInterval #(swap! seconds-left dec) 1000)
(fn []
[:div.timer
[:div "Time Remaining: " (show-time #seconds-left)]
[reset-component seconds-left]])))
Notice that now call to setInterval happens only at component initialization and not during the render phase.

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.