From 8e137ec24a586d4529d04e222c579c61657fa85a Mon Sep 17 00:00:00 2001 From: Juho Teperi Date: Mon, 18 Jun 2018 00:47:10 +0300 Subject: [PATCH] Use React state to manage controlled inputs --- examples/material-ui/checkouts/reagent | 1 + examples/material-ui/src/example/core.cljs | 35 +---- examples/todomvc/src/todomvc/core.cljs | 2 +- src/reagent/impl/template.cljs | 168 ++++++--------------- 4 files changed, 48 insertions(+), 158 deletions(-) create mode 120000 examples/material-ui/checkouts/reagent diff --git a/examples/material-ui/checkouts/reagent b/examples/material-ui/checkouts/reagent new file mode 120000 index 0000000..1b20c9f --- /dev/null +++ b/examples/material-ui/checkouts/reagent @@ -0,0 +1 @@ +../../../ \ No newline at end of file diff --git a/examples/material-ui/src/example/core.cljs b/examples/material-ui/src/example/core.cljs index 5471f93..cb7bae1 100644 --- a/examples/material-ui/src/example/core.cljs +++ b/examples/material-ui/src/example/core.cljs @@ -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" diff --git a/examples/todomvc/src/todomvc/core.cljs b/examples/todomvc/src/todomvc/core.cljs index 93ed17d..67bc757 100644 --- a/examples/todomvc/src/todomvc/core.cljs +++ b/examples/todomvc/src/todomvc/core.cljs @@ -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)] diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs index 7dfa83f..f268bce 100644 --- a/src/reagent/impl/template.cljs +++ b/src/reagent/impl/template.cljs @@ -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))]