mirror of https://github.com/status-im/reagent.git
Merge pull request #282 from arbscht/synthetic-input
Support synthetic text input components
This commit is contained in:
commit
35908d142d
|
@ -50,9 +50,11 @@
|
||||||
(defn adapt-react-class
|
(defn adapt-react-class
|
||||||
"Returns an adapter for a native React class, that may be used
|
"Returns an adapter for a native React class, that may be used
|
||||||
just like a Reagent component function or class in Hiccup forms."
|
just like a Reagent component function or class in Hiccup forms."
|
||||||
[c]
|
([c opts]
|
||||||
(assert-some c "Component")
|
(assert-some c "Component")
|
||||||
(tmpl/adapt-react-class c))
|
(tmpl/adapt-react-class c opts))
|
||||||
|
([c]
|
||||||
|
(adapt-react-class c {})))
|
||||||
|
|
||||||
(defn reactify-component
|
(defn reactify-component
|
||||||
"Returns an adapter for a Reagent component, that may be used from
|
"Returns an adapter for a Reagent component, that may be used from
|
||||||
|
|
|
@ -120,55 +120,68 @@
|
||||||
[input-type]
|
[input-type]
|
||||||
(contains? these-inputs-have-selection-api input-type))
|
(contains? these-inputs-have-selection-api input-type))
|
||||||
|
|
||||||
(defn input-set-value [this]
|
(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)
|
(when ($ this :cljsInputLive)
|
||||||
($! this :cljsInputDirty false)
|
($! this :cljsInputDirty false)
|
||||||
(let [rendered-value ($ this :cljsRenderedValue)
|
(let [rendered-value ($ this :cljsRenderedValue)
|
||||||
dom-value ($ this :cljsDOMValue)
|
dom-value ($ this :cljsDOMValue)
|
||||||
node (find-dom-node this)]
|
node (find-dom-node this) ;; Default to the root node within this component
|
||||||
|
synthetic-on-update ($ this :cljsSyntheticOnUpdate)]
|
||||||
(when (not= rendered-value dom-value)
|
(when (not= rendered-value dom-value)
|
||||||
(if-not (and (identical? node ($ js/document :activeElement))
|
(if (fn? synthetic-on-update)
|
||||||
(has-selection-api? ($ node :type))
|
(synthetic-on-update input-node-set-value node rendered-value dom-value this)
|
||||||
(string? rendered-value)
|
(input-node-set-value node rendered-value dom-value this {}))))))
|
||||||
(string? dom-value))
|
|
||||||
;; just set the value, no need to worry about a cursor
|
|
||||||
(do
|
|
||||||
($! this :cljsDOMValue rendered-value)
|
|
||||||
($! node :value 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-set-value this))
|
|
||||||
(let [existing-offset-from-end (- (count node-value)
|
|
||||||
($ node :selectionStart))
|
|
||||||
new-cursor-offset (- (count rendered-value)
|
|
||||||
existing-offset-from-end)]
|
|
||||||
($! this :cljsDOMValue rendered-value)
|
|
||||||
($! node :value rendered-value)
|
|
||||||
($! node :selectionStart new-cursor-offset)
|
|
||||||
($! node :selectionEnd new-cursor-offset)))))))))
|
|
||||||
|
|
||||||
(defn input-handle-change [this on-change e]
|
(defn input-handle-change [this on-change e]
|
||||||
($! this :cljsDOMValue (-> e .-target .-value))
|
($! this :cljsDOMValue (-> e .-target .-value))
|
||||||
|
@ -176,29 +189,38 @@
|
||||||
;; wants to keep the value unchanged
|
;; wants to keep the value unchanged
|
||||||
(when-not ($ this :cljsInputDirty)
|
(when-not ($ this :cljsInputDirty)
|
||||||
($! this :cljsInputDirty true)
|
($! this :cljsInputDirty true)
|
||||||
(batch/do-after-render #(input-set-value this)))
|
(batch/do-after-render #(input-component-set-value this)))
|
||||||
(on-change e))
|
(on-change e))
|
||||||
|
|
||||||
(defn input-render-setup [this jsprops]
|
(defn input-render-setup
|
||||||
;; Don't rely on React for updating "controlled inputs", since it
|
([this jsprops {:keys [synthetic-on-update synthetic-on-change]}]
|
||||||
;; doesn't play well with async rendering (misses keystrokes).
|
;; Don't rely on React for updating "controlled inputs", since it
|
||||||
(when (and (some? jsprops)
|
;; doesn't play well with async rendering (misses keystrokes).
|
||||||
(.hasOwnProperty jsprops "onChange")
|
(when (and (some? jsprops)
|
||||||
(.hasOwnProperty jsprops "value"))
|
(.hasOwnProperty jsprops "onChange")
|
||||||
(assert find-dom-node
|
(.hasOwnProperty jsprops "value"))
|
||||||
"reagent.dom needs to be loaded for controlled input to work")
|
(assert find-dom-node
|
||||||
(let [v ($ jsprops :value)
|
"reagent.dom needs to be loaded for controlled input to work")
|
||||||
value (if (nil? v) "" v)
|
(when synthetic-on-update
|
||||||
on-change ($ jsprops :onChange)]
|
;; Pass along any synthetic input setter given
|
||||||
(when-not ($ this :cljsInputLive)
|
($! this :cljsSyntheticOnUpdate synthetic-on-update))
|
||||||
;; set initial value
|
(let [v ($ jsprops :value)
|
||||||
($! this :cljsInputLive true)
|
value (if (nil? v) "" v)
|
||||||
($! this :cljsDOMValue value))
|
on-change ($ jsprops :onChange)
|
||||||
($! this :cljsRenderedValue value)
|
on-change (if synthetic-on-change
|
||||||
(js-delete jsprops "value")
|
(partial synthetic-on-change on-change)
|
||||||
(doto jsprops
|
on-change)]
|
||||||
($! :defaultValue value)
|
(when-not ($ this :cljsInputLive)
|
||||||
($! :onChange #(input-handle-change this on-change %))))))
|
;; 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 %))))))
|
||||||
|
([this jsprops]
|
||||||
|
(input-render-setup this jsprops {})))
|
||||||
|
|
||||||
(defn input-unmount [this]
|
(defn input-unmount [this]
|
||||||
($! this :cljsInputLive nil))
|
($! this :cljsInputLive nil))
|
||||||
|
@ -210,11 +232,13 @@
|
||||||
|
|
||||||
(def reagent-input-class nil)
|
(def reagent-input-class nil)
|
||||||
|
|
||||||
|
(def reagent-synthetic-input-class nil)
|
||||||
|
|
||||||
(declare make-element)
|
(declare make-element)
|
||||||
|
|
||||||
(def input-spec
|
(def input-spec
|
||||||
{:display-name "ReagentInput"
|
{:display-name "ReagentInput"
|
||||||
:component-did-update input-set-value
|
:component-did-update input-component-set-value
|
||||||
:component-will-unmount input-unmount
|
:component-will-unmount input-unmount
|
||||||
:reagent-render
|
:reagent-render
|
||||||
(fn [argv comp jsprops first-child]
|
(fn [argv comp jsprops first-child]
|
||||||
|
@ -222,11 +246,31 @@
|
||||||
(input-render-setup this jsprops)
|
(input-render-setup this jsprops)
|
||||||
(make-element argv comp jsprops first-child)))})
|
(make-element argv comp jsprops first-child)))})
|
||||||
|
|
||||||
(defn reagent-input []
|
(def synthetic-input-spec
|
||||||
|
;; Same as `input-spec` except it takes another argument for `input-setter`
|
||||||
|
{:display-name "ReagentSyntheticInput"
|
||||||
|
:component-did-update input-component-set-value
|
||||||
|
:component-will-unmount input-unmount
|
||||||
|
:reagent-render
|
||||||
|
(fn [on-update on-change argv comp jsprops first-child]
|
||||||
|
(let [this comp/*current-component*]
|
||||||
|
(input-render-setup this jsprops {:synthetic-on-update on-update
|
||||||
|
:synthetic-on-change on-change})
|
||||||
|
(make-element argv comp jsprops first-child)))})
|
||||||
|
|
||||||
|
|
||||||
|
(defn reagent-input
|
||||||
|
[]
|
||||||
(when (nil? reagent-input-class)
|
(when (nil? reagent-input-class)
|
||||||
(set! reagent-input-class (comp/create-class input-spec)))
|
(set! reagent-input-class (comp/create-class input-spec)))
|
||||||
reagent-input-class)
|
reagent-input-class)
|
||||||
|
|
||||||
|
(defn reagent-synthetic-input
|
||||||
|
[]
|
||||||
|
(when (nil? reagent-synthetic-input-class)
|
||||||
|
(set! reagent-synthetic-input-class (comp/create-class synthetic-input-spec)))
|
||||||
|
reagent-synthetic-input-class)
|
||||||
|
|
||||||
|
|
||||||
;;; Conversion from Hiccup forms
|
;;; Conversion from Hiccup forms
|
||||||
|
|
||||||
|
@ -262,11 +306,39 @@
|
||||||
($! jsprops :key key))
|
($! jsprops :key key))
|
||||||
(react/createElement c jsprops)))
|
(react/createElement c jsprops)))
|
||||||
|
|
||||||
(defn adapt-react-class [c]
|
(defn adapt-react-class
|
||||||
(doto (->NativeWrapper)
|
([c {:keys [synthetic-input]}]
|
||||||
($! :name c)
|
(let [on-update (:on-update synthetic-input)
|
||||||
($! :id nil)
|
on-change (:on-change synthetic-input)]
|
||||||
($! :class nil)))
|
(when synthetic-input
|
||||||
|
(assert (fn? on-update))
|
||||||
|
(assert (fn? on-change)))
|
||||||
|
(let [wrapped (doto (->NativeWrapper)
|
||||||
|
($! :name c)
|
||||||
|
($! :id nil)
|
||||||
|
($! :class nil))
|
||||||
|
wrapped (if synthetic-input
|
||||||
|
(doto wrapped
|
||||||
|
($! :syntheticInput true))
|
||||||
|
wrapped)
|
||||||
|
wrapped (if synthetic-input
|
||||||
|
(doto wrapped
|
||||||
|
($! :syntheticOnChange on-change))
|
||||||
|
wrapped)
|
||||||
|
wrapped (if synthetic-input
|
||||||
|
;; This is a synthetic input component, i.e. it has a complex
|
||||||
|
;; nesting of elements such that the root node is not necessarily
|
||||||
|
;; the <input> tag we need to control, and/or it needs to execute
|
||||||
|
;; custom code when updated values are written so we provide an affordance
|
||||||
|
;; to configure a setter fn that can choose a different DOM node
|
||||||
|
;; than the root node if it wants, and can supply a function hooked
|
||||||
|
;; to value updates so it can maintain its own component state as needed.
|
||||||
|
(doto wrapped
|
||||||
|
($! :syntheticOnUpdate on-update))
|
||||||
|
wrapped)]
|
||||||
|
wrapped)))
|
||||||
|
([c]
|
||||||
|
(adapt-react-class c {})))
|
||||||
|
|
||||||
(def tag-name-cache #js{})
|
(def tag-name-cache #js{})
|
||||||
|
|
||||||
|
@ -278,13 +350,24 @@
|
||||||
(declare as-element)
|
(declare as-element)
|
||||||
|
|
||||||
(defn native-element [parsed argv first]
|
(defn native-element [parsed argv first]
|
||||||
(let [comp ($ parsed :name)]
|
(let [comp ($ parsed :name)
|
||||||
|
synthetic-input ($ parsed :syntheticInput)]
|
||||||
(let [props (nth argv first nil)
|
(let [props (nth argv first nil)
|
||||||
hasprops (or (nil? props) (map? props))
|
hasprops (or (nil? props) (map? props))
|
||||||
jsprops (convert-props (if hasprops props) parsed)
|
jsprops (convert-props (if hasprops props) parsed)
|
||||||
first-child (+ first (if hasprops 1 0))]
|
first-child (+ first (if hasprops 1 0))]
|
||||||
(if (input-component? comp)
|
(if (or synthetic-input (input-component? comp))
|
||||||
(-> [(reagent-input) argv comp jsprops first-child]
|
(-> (if synthetic-input
|
||||||
|
;; If we are dealing with a synthetic input, use the synthetic-input-spec form:
|
||||||
|
[(reagent-synthetic-input)
|
||||||
|
($ parsed :syntheticOnUpdate)
|
||||||
|
($ parsed :syntheticOnChange)
|
||||||
|
argv
|
||||||
|
comp
|
||||||
|
jsprops
|
||||||
|
first-child]
|
||||||
|
;; Else use the regular input-spec form:
|
||||||
|
[(reagent-input) argv comp jsprops first-child])
|
||||||
(with-meta (meta argv))
|
(with-meta (meta argv))
|
||||||
as-element)
|
as-element)
|
||||||
(let [key (-> (meta argv) get-key)
|
(let [key (-> (meta argv) get-key)
|
||||||
|
|
Loading…
Reference in New Issue