mirror of
https://github.com/status-im/reagent.git
synced 2025-01-12 12:54:50 +00:00
Use React state to manage controlled inputs
This commit is contained in:
parent
dca635e3b0
commit
8e137ec24a
1
examples/material-ui/checkouts/reagent
Symbolic link
1
examples/material-ui/checkouts/reagent
Symbolic link
@ -0,0 +1 @@
|
||||
../../../
|
@ -7,31 +7,7 @@
|
||||
(def mui-theme-provider (r/adapt-react-class mui/MuiThemeProvider))
|
||||
(def menu-item (r/adapt-react-class mui/MenuItem))
|
||||
|
||||
(defn adapt-input-component [component]
|
||||
(fn [props & _]
|
||||
(r/create-class
|
||||
{:getInitialState (fn [] #js {:value (:value props)})
|
||||
:component-will-receive-props
|
||||
(fn [this [_ next-props]]
|
||||
(when (not= (:value next-props) (.-value (.-state this)))
|
||||
(.setState this #js {:value (:value next-props)})))
|
||||
:should-component-update
|
||||
(fn [this old-argv new-argv]
|
||||
true)
|
||||
:reagent-render
|
||||
(fn [props & children]
|
||||
(this-as this
|
||||
(let [props (-> props
|
||||
(cond-> (:on-change props)
|
||||
(assoc :on-change (fn [e]
|
||||
(.setState this #js {:value (.. e -target -value)})
|
||||
((:on-change props) e))))
|
||||
(cond-> (:value props)
|
||||
(assoc :value (.-value (.-state this))))
|
||||
rtpl/convert-prop-value)]
|
||||
(apply r/create-element component props (map r/as-element children)))))}) ))
|
||||
|
||||
(def text-field (adapt-input-component mui/TextField))
|
||||
(def text-field (rtpl/adapt-input-component mui/TextField))
|
||||
|
||||
(defonce text-state (r/atom "foobar"))
|
||||
|
||||
@ -54,8 +30,7 @@
|
||||
"reset"]
|
||||
|
||||
[text-field
|
||||
{:id "example"
|
||||
:value @text-state
|
||||
{:value @text-state
|
||||
:label "Text input"
|
||||
:placeholder "Placeholder"
|
||||
:helper-text "Helper text"
|
||||
@ -64,8 +39,7 @@
|
||||
:inputRef #(js/console.log "input-ref" %)}]
|
||||
|
||||
[text-field
|
||||
{:id "example"
|
||||
:value @text-state
|
||||
{:value @text-state
|
||||
:label "Textarea"
|
||||
:placeholder "Placeholder"
|
||||
:helper-text "Helper text"
|
||||
@ -74,8 +48,7 @@
|
||||
:multiline true}]
|
||||
|
||||
[text-field
|
||||
{:id "example"
|
||||
:value @text-state
|
||||
{:value @text-state
|
||||
:label "Select"
|
||||
:placeholder "Placeholder"
|
||||
:helper-text "Helper text"
|
||||
|
@ -25,7 +25,7 @@
|
||||
(complete-all true)))
|
||||
|
||||
(defn todo-input [{:keys [title on-save on-stop]}]
|
||||
(let [val (r/atom title)
|
||||
(let [val (r/atom (or title ""))
|
||||
stop #(do (reset! val "")
|
||||
(if on-stop (on-stop)))
|
||||
save #(let [v (-> @val str clojure.string/trim)]
|
||||
|
@ -161,126 +161,43 @@
|
||||
[input-type]
|
||||
(contains? these-inputs-have-selection-api input-type))
|
||||
|
||||
(declare input-component-set-value)
|
||||
|
||||
(defn input-node-set-value
|
||||
[node rendered-value dom-value component {:keys [on-write]}]
|
||||
(if-not (and (identical? node ($ js/document :activeElement))
|
||||
(has-selection-api? ($ node :type))
|
||||
(string? rendered-value)
|
||||
(string? dom-value))
|
||||
;; just set the value, no need to worry about a cursor
|
||||
(do
|
||||
($! component :cljsDOMValue rendered-value)
|
||||
($! node :value rendered-value)
|
||||
(when (fn? on-write)
|
||||
(on-write rendered-value)))
|
||||
|
||||
;; Setting "value" (below) moves the cursor position to the
|
||||
;; end which gives the user a jarring experience.
|
||||
;;
|
||||
;; But repositioning the cursor within the text, turns out to
|
||||
;; be quite a challenge because changes in the text can be
|
||||
;; triggered by various events like:
|
||||
;; - a validation function rejecting a user inputted char
|
||||
;; - the user enters a lower case char, but is transformed to
|
||||
;; upper.
|
||||
;; - the user selects multiple chars and deletes text
|
||||
;; - the user pastes in multiple chars, and some of them are
|
||||
;; rejected by a validator.
|
||||
;; - the user selects multiple chars and then types in a
|
||||
;; single new char to repalce them all.
|
||||
;; Coming up with a sane cursor repositioning strategy hasn't
|
||||
;; been easy ALTHOUGH in the end, it kinda fell out nicely,
|
||||
;; and it appears to sanely handle all the cases we could
|
||||
;; think of.
|
||||
;; So this is just a warning. The code below is simple
|
||||
;; enough, but if you are tempted to change it, be aware of
|
||||
;; all the scenarios you have handle.
|
||||
(let [node-value ($ node :value)]
|
||||
(if (not= node-value dom-value)
|
||||
;; IE has not notified us of the change yet, so check again later
|
||||
(batch/do-after-render #(input-component-set-value component))
|
||||
(let [existing-offset-from-end (- (count node-value)
|
||||
($ node :selectionStart))
|
||||
new-cursor-offset (- (count rendered-value)
|
||||
existing-offset-from-end)]
|
||||
($! component :cljsDOMValue rendered-value)
|
||||
($! node :value rendered-value)
|
||||
(when (fn? on-write)
|
||||
(on-write rendered-value))
|
||||
($! node :selectionStart new-cursor-offset)
|
||||
($! node :selectionEnd new-cursor-offset))))))
|
||||
|
||||
(defn input-component-set-value [this]
|
||||
(when ($ this :cljsInputLive)
|
||||
($! this :cljsInputDirty false)
|
||||
(let [rendered-value ($ this :cljsRenderedValue)
|
||||
dom-value ($ this :cljsDOMValue)
|
||||
;; Default to the root node within this component
|
||||
node (find-dom-node this)]
|
||||
(when (not= rendered-value dom-value)
|
||||
(input-node-set-value node rendered-value dom-value this {})))))
|
||||
|
||||
(defn input-handle-change [this on-change e]
|
||||
($! this :cljsDOMValue (-> e .-target .-value))
|
||||
;; Make sure the input is re-rendered, in case on-change
|
||||
;; wants to keep the value unchanged
|
||||
(when-not ($ this :cljsInputDirty)
|
||||
($! this :cljsInputDirty true)
|
||||
(batch/do-after-render #(input-component-set-value this)))
|
||||
(on-change e))
|
||||
|
||||
(defn input-render-setup
|
||||
[this jsprops]
|
||||
;; Don't rely on React for updating "controlled inputs", since it
|
||||
;; doesn't play well with async rendering (misses keystrokes).
|
||||
(when (and (some? jsprops)
|
||||
(.hasOwnProperty jsprops "onChange")
|
||||
(.hasOwnProperty jsprops "value"))
|
||||
(assert find-dom-node
|
||||
"reagent.dom needs to be loaded for controlled input to work")
|
||||
(let [v ($ jsprops :value)
|
||||
value (if (nil? v) "" v)
|
||||
on-change ($ jsprops :onChange)]
|
||||
(when-not ($ this :cljsInputLive)
|
||||
;; set initial value
|
||||
($! this :cljsInputLive true)
|
||||
($! this :cljsDOMValue value))
|
||||
($! this :cljsRenderedValue value)
|
||||
(js-delete jsprops "value")
|
||||
(doto jsprops
|
||||
($! :defaultValue value)
|
||||
($! :onChange #(input-handle-change this on-change %))))))
|
||||
|
||||
(defn input-unmount [this]
|
||||
($! this :cljsInputLive nil))
|
||||
|
||||
(defn ^boolean input-component? [x]
|
||||
(case x
|
||||
("input" "textarea") true
|
||||
false))
|
||||
|
||||
(def reagent-input-class nil)
|
||||
(defn adapt-input-component [component]
|
||||
(fn [props & _]
|
||||
(comp/create-class
|
||||
{:display-name "InputWrapper"
|
||||
:get-initial-state
|
||||
(fn []
|
||||
#js {:value (:value props)})
|
||||
:should-component-update
|
||||
(fn [this old-argv new-args]
|
||||
true)
|
||||
:component-will-receive-props
|
||||
(fn [this [_ props]]
|
||||
(when (not= (:value props) (.. this -state -value))
|
||||
(.setState this #js {:value (:value props)})))
|
||||
:reagent-render
|
||||
(fn [props & children]
|
||||
(this-as this
|
||||
(let [props (if (or (not= "input" component)
|
||||
(has-selection-api? (:type props)))
|
||||
(-> props
|
||||
(cond-> (:on-change props)
|
||||
(assoc :on-change (fn [e]
|
||||
(.setState this #js {:value (.. e -target -value)})
|
||||
((:on-change props) e))))
|
||||
(cond-> (.. this -state -value)
|
||||
(assoc :value (.. this -state -value)))
|
||||
convert-prop-value)
|
||||
(convert-prop-value props))]
|
||||
(apply react/createElement component props (map as-element children)))))})))
|
||||
|
||||
(declare make-element)
|
||||
|
||||
(def input-spec
|
||||
{:display-name "ReagentInput"
|
||||
:component-did-update input-component-set-value
|
||||
:component-will-unmount input-unmount
|
||||
:reagent-render
|
||||
(fn [argv comp jsprops first-child]
|
||||
(let [this comp/*current-component*]
|
||||
(input-render-setup this jsprops)
|
||||
(make-element argv comp jsprops first-child)))})
|
||||
|
||||
(defn reagent-input
|
||||
[]
|
||||
(when (nil? reagent-input-class)
|
||||
(set! reagent-input-class (comp/create-class input-spec)))
|
||||
reagent-input-class)
|
||||
(def reagent-input
|
||||
(adapt-input-component "input"))
|
||||
|
||||
(def reagent-textarea
|
||||
(adapt-input-component "textarea"))
|
||||
|
||||
;;; Conversion from Hiccup forms
|
||||
|
||||
@ -343,16 +260,15 @@
|
||||
(aset tag-name-cache x (parse-tag x))))
|
||||
|
||||
(defn native-element [parsed argv first]
|
||||
(let [comp ($ parsed :name)
|
||||
props (nth argv first nil)
|
||||
hasprops (or (nil? props) (map? props))
|
||||
jsprops (convert-props (if hasprops props) parsed)
|
||||
first-child (+ first (if hasprops 1 0))]
|
||||
(if (input-component? comp)
|
||||
(-> [(reagent-input) argv comp jsprops first-child]
|
||||
(with-meta (meta argv))
|
||||
as-element)
|
||||
(let [key (-> (meta argv) get-key)
|
||||
(let [comp ($ parsed :name)]
|
||||
(case comp
|
||||
"input" (reag-element reagent-input argv)
|
||||
"textarea" (reag-element reagent-textarea argv)
|
||||
(let [props (nth argv first nil)
|
||||
hasprops (or (nil? props) (map? props))
|
||||
jsprops (convert-props (if hasprops props) parsed)
|
||||
first-child (+ first (if hasprops 1 0))
|
||||
key (-> (meta argv) get-key)
|
||||
p (if (nil? key)
|
||||
jsprops
|
||||
(oset jsprops "key" key))]
|
||||
|
Loading…
x
Reference in New Issue
Block a user