Hi! My code below works, but can it be implemented less idiotic way? π
(defn- user-answer [right-answer on-success on-fail]
(let [local-state (r/atom nil)
check-fn (fn [] (when-let [answer (some-> @local-state :user-input str str/trim not-empty)]
(let [real-right-answer (:right-answer @local-state)] ;; can't rely on right-answer here!
(prn answer real-right-answer)
(if (= answer real-right-answer)
(on-success)
(on-fail))
(swap! local-state dissoc :user-input))))]
(r/create-class
{:component-did-mount
(fn [this]
(let [initial-right-answer (second (r/argv this))]
(prn :mount initial-right-answer)
(swap! local-state assoc :right-answer initial-right-answer)) ;; catch right-answer into local-state
(when-let [input (.getElementById js/document "user-input")]
(.addEventListener input "keyup" (fn [e] (when (= 13 (.-keyCode e)) (check-fn))))))
:component-did-update (fn [this old-argv old-state snapshot]
(let [updated-right-answer (second (r/argv this))]
(prn :update updated-right-answer)
(swap! local-state assoc :right-answer updated-right-answer))) ;; update right-answer in local-state
:reagent-render
(fn [right-answer on-success on-fail]
[:div
[:span (str "hint-from-render: " right-answer)] " "
[:span (str "hint-from-ratom: " (:right-answer @local-state))] " "
[:input {:type "text"
:id "user-input"
:value (:user-input @local-state)
:on-change #(swap! local-state assoc :user-input (.. % -target -value))
:auto-complete :off}]
[:button {:on-click check-fn
:disabled (str/blank? (:user-input @local-state))}
"Check answer"]])})))Don't use getElementById, use React refs instead.
No need to set the initial value of local-state in :component-did-mount - you can do it right in the top let since the values of props won't change between those two points.
No need to use .addEventListener explicitly - you can add a :on-key-up property to :input.
Maybe I'm missing something - why did you split :on-change and "keyup"? Why not handle everything in :on-change?
My thoughts was that: I need to force check on input press enter key => I need addEventListener => I need element => I need 3-rd form and :componentDidMount. I'l try your suggestions and share the results
Ah, didn't notice the check for code 13.
I'd replace the top div with a form and do everything in its :on-submit, along with preventing the default action of the event.
In the end, you'll probably end up with a form-2 component. I don't see how a form-3 one would be needed here.
If I use addEventListener I need element for finding by id and guarantee that it is already in DOM, so I need :component-did-mount event and add listener there
I thought about form & submit but have no experience of using forms with actions other that fetch or open dialogs. Meybe I can use on-submit & preventDefault
But before form implementation - thanks, :on-key-up works! And I do not need event listeners and 3-rd form π
This works, and it looks much better that initial impl
(defn- user-answer [right-answer on-success on-fail]
(r/with-let [answer (r/atom nil)]
(let [check-fn (fn [] (when-let [ans (some-> @answer str not-empty)]
(prn ans right-answer)
(if (= ans right-answer)
(on-success)
(on-fail))
(reset! answer nil)))]
[:div
[:span (str "hint " right-answer " ")]
[:input {:type "text"
:value @answer
:on-change #(reset! answer (.. % -target -value))
:on-key-up (fn [e] (when (= 13 (.-keyCode e)) (check-fn)))}]
[:button {:on-click check-fn
:disabled (str/blank? @answer)}
"ΠΡΠΎΠ²Π΅ΡΠΈΡΡ ΠΎΡΠ²Π΅Ρ"]])))> If I use addEventListener I need element for finding by id and guarantee that it is already in DOM
Yes.
> so I need :component-did-mount event and add listener there
Not necessarily, refs can help here.
> Meybe I can use on-submit & preventDefault
Yes. Would also remove the need to check for 13 and call check-fn in two places (forms are submitted automatically when the <button> there is pressed or when Enter is hit with the focus being in one of the form's inputs).
https://github.com/reagent-project/reagent/blob/master/doc/FAQ/UsingRefs.md reagent docs on refs, they do indeed work much better here than getElementById
On first render the atom is nil, but once it renders the atom is swapped and then always has the ref. A simple when @ref check is all that's needed to ensure existence
Thanks alot, I already have much better version cause of :on-key-up and will go further with form & on-submit
Even form-1 without refs (still don't understand them properly) and with some googled form- and style- magic π
(defn- user-answer [right-answer on-success on-fail]
[:form {:on-submit (fn [e]
(.preventDefault e)
(when-let [answer (some-> e .-target js/FormData. (.get "input") str not-empty)]
(if (= answer right-answer)
(on-success)
(on-fail))
(-> e .-target .reset)))}
[:span (str "hint " right-answer " ")]
;;
;;
[:style "input:invalid + button { opacity: 0.3; pointer-events: none; }"]
[:input {:name "input"
:required true
:auto-complete :off}]
[:button "Check answer"]]) Nice.
A few other things:
β’ No need to call str in :span - you can use [:span "hint " right answer " "]
β’ :style is technically not valid outside of :head and some other metadata-accepting elements, but browsers still accept it
β’ That :style affects the whole page, not just the user-answer component
Yep, but still can't get worked inline map-based hiccup :style {...} with pseudo-classes
A proper CSS/SCSS/whatever file would be best. :) Alternatively, you can use a form-2 component and make inline styles conditional.
Oh, and since you rely on :required true, you should use checkValidity instead of re-checking the value yourself.
If I move that style to root css file (or in header of index.html) it will be applyed to all the page again, right? And with form-2 I may have atoms with refs, ratoms with set/get value of input and avoid style magic & set :disabled on button directly π
I'l google on checkValidity , thanks. Looks like it form's attribute/method, not input's one
> it will be applyed to all the page again, right?
Yes, but it won't be "technically incorrect" as with that :style and you'll be able to use a custom :class to help scope the value. There are libraries that do stuff like that at run time for you.
Thanks, I'l create that root class for forms style, if could
For now finished with css
form input:invalid ~ button {
opacity: 0.4;
pointer-events: none;
}
cljs
(defn- user-answer [right-answer on-success on-fail]
[:form {:on-submit (fn [e]
(.preventDefault e)
(when-let [answer (some-> e .-target js/FormData. (.get "input") str str/trim not-empty)]
(if (= answer right-answer)
(on-success)
(on-fail)))
(-> e .-target .reset))}
[:span "hint: " right-answer " "]
[:input {:name "input"
:required true
:auto-complete :off}] " "
[:button "Check answer"]])
Works fine, checks only non-empty & non-blank inputs, but resets blanks. I don't check form's validity cause anyway I have to extract input value, so it is enough to chek it only.
Thanks alot, it's much better than my initial solution π