mirror of https://github.com/status-im/reagent.git
Move input workaround to separate ns
This commit is contained in:
parent
885664b04d
commit
edc318749c
|
@ -2,6 +2,7 @@
|
||||||
(:require [react-dom :as react-dom]
|
(:require [react-dom :as react-dom]
|
||||||
[reagent.impl.util :as util]
|
[reagent.impl.util :as util]
|
||||||
[reagent.impl.template :as tmpl]
|
[reagent.impl.template :as tmpl]
|
||||||
|
[reagent.impl.input :as input]
|
||||||
[reagent.impl.batching :as batch]
|
[reagent.impl.batching :as batch]
|
||||||
[reagent.ratom :as ratom]))
|
[reagent.ratom :as ratom]))
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@
|
||||||
[this]
|
[this]
|
||||||
(react-dom/findDOMNode this))
|
(react-dom/findDOMNode this))
|
||||||
|
|
||||||
(set! tmpl/find-dom-node dom-node)
|
(set! input/find-dom-node dom-node)
|
||||||
|
|
||||||
(defn force-update-all
|
(defn force-update-all
|
||||||
"Force re-rendering of all mounted Reagent components. This is
|
"Force re-rendering of all mounted Reagent components. This is
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
(ns reagent.impl.input
|
||||||
|
(:require [reagent.impl.component :as comp]
|
||||||
|
[reagent.impl.batching :as batch]))
|
||||||
|
|
||||||
|
;; This gets set from reagent.dom
|
||||||
|
;; No direct reference to reagent.dom as we don't want to load react-dom
|
||||||
|
;; for non dom targets.
|
||||||
|
(defonce find-dom-node nil)
|
||||||
|
|
||||||
|
;; Workaround circular dependency
|
||||||
|
(defonce make-element nil)
|
||||||
|
|
||||||
|
;; <input type="??" >
|
||||||
|
;; The properites 'selectionStart' and 'selectionEnd' only exist on some inputs
|
||||||
|
;; See: https://html.spec.whatwg.org/multipage/forms.html#do-not-apply
|
||||||
|
(def these-inputs-have-selection-api #{"text" "textarea" "password" "search"
|
||||||
|
"tel" "url"})
|
||||||
|
|
||||||
|
(defn ^boolean has-selection-api?
|
||||||
|
[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 ^clj component {:keys [on-write]}]
|
||||||
|
(if-not (and (identical? node (.-activeElement js/document))
|
||||||
|
(has-selection-api? (.-type node))
|
||||||
|
(string? rendered-value)
|
||||||
|
(string? dom-value))
|
||||||
|
;; just set the value, no need to worry about a cursor
|
||||||
|
(do
|
||||||
|
(set! (.-cljsDOMValue component) rendered-value)
|
||||||
|
(set! (.-value node) 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 (.-value node)]
|
||||||
|
(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)
|
||||||
|
(.-selectionStart node))
|
||||||
|
new-cursor-offset (- (count rendered-value)
|
||||||
|
existing-offset-from-end)]
|
||||||
|
(set! (.-cljsDOMValue component) rendered-value)
|
||||||
|
(set! (.-value node) rendered-value)
|
||||||
|
(when (fn? on-write)
|
||||||
|
(on-write rendered-value))
|
||||||
|
(set! (.-selectionStart node) new-cursor-offset)
|
||||||
|
(set! (.-selectionEnd node) new-cursor-offset))))))
|
||||||
|
|
||||||
|
(defn input-component-set-value [^clj this]
|
||||||
|
(when (.-cljsInputLive this)
|
||||||
|
(set! (.-cljsInputDirty this) false)
|
||||||
|
(let [rendered-value (.-cljsRenderedValue this)
|
||||||
|
dom-value (.-cljsDOMValue this)
|
||||||
|
;; 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 [^clj this on-change e]
|
||||||
|
(set! (.-cljsDOMValue this) (-> e .-target .-value))
|
||||||
|
;; Make sure the input is re-rendered, in case on-change
|
||||||
|
;; wants to keep the value unchanged
|
||||||
|
(when-not (.-cljsInputDirty this)
|
||||||
|
(set! (.-cljsInputDirty this) true)
|
||||||
|
(batch/do-after-render #(input-component-set-value this)))
|
||||||
|
(on-change e))
|
||||||
|
|
||||||
|
(defn input-render-setup
|
||||||
|
[^clj this ^js 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 (.-value jsprops)
|
||||||
|
value (if (nil? v) "" v)
|
||||||
|
on-change (.-onChange jsprops)]
|
||||||
|
(when-not (.-cljsInputLive this)
|
||||||
|
;; set initial value
|
||||||
|
(set! (.-cljsInputLive this) true)
|
||||||
|
(set! (.-cljsDOMValue this) value))
|
||||||
|
(set! (.-cljsRenderedValue this) value)
|
||||||
|
(js-delete jsprops "value")
|
||||||
|
(set! (.-defaultValue jsprops) value)
|
||||||
|
(set! (.-onChange jsprops) #(input-handle-change this on-change %)))))
|
||||||
|
|
||||||
|
(defn input-unmount [^clj this]
|
||||||
|
(set! (.-cljsInputLive this) nil))
|
||||||
|
|
||||||
|
(defn ^boolean input-component? [x]
|
||||||
|
(case x
|
||||||
|
("input" "textarea") true
|
||||||
|
false))
|
||||||
|
|
||||||
|
(def reagent-input-class nil)
|
||||||
|
|
||||||
|
(def input-spec
|
||||||
|
{:display-name "ReagentInput"
|
||||||
|
:component-did-update input-component-set-value
|
||||||
|
:component-will-unmount input-unmount
|
||||||
|
:reagent-render
|
||||||
|
(fn [argv component jsprops first-child opts]
|
||||||
|
(let [this comp/*current-component*]
|
||||||
|
(input-render-setup this jsprops)
|
||||||
|
(make-element argv component jsprops first-child opts)))})
|
||||||
|
|
||||||
|
(defn reagent-input
|
||||||
|
[]
|
||||||
|
(when (nil? reagent-input-class)
|
||||||
|
(set! reagent-input-class (comp/create-class input-spec)))
|
||||||
|
reagent-input-class)
|
|
@ -5,11 +5,12 @@
|
||||||
[reagent.impl.util :as util :refer [named?]]
|
[reagent.impl.util :as util :refer [named?]]
|
||||||
[reagent.impl.component :as comp]
|
[reagent.impl.component :as comp]
|
||||||
[reagent.impl.batching :as batch]
|
[reagent.impl.batching :as batch]
|
||||||
|
[reagent.impl.input :as input]
|
||||||
[reagent.ratom :as ratom]
|
[reagent.ratom :as ratom]
|
||||||
[reagent.debug :refer-macros [dev? warn]]
|
[reagent.debug :refer-macros [dev? warn]]
|
||||||
[goog.object :as gobj]))
|
[goog.object :as gobj]))
|
||||||
|
|
||||||
(declare as-element)
|
(declare as-element make-element)
|
||||||
|
|
||||||
;; From Weavejester's Hiccup, via pump:
|
;; From Weavejester's Hiccup, via pump:
|
||||||
(def ^{:doc "Regular expression that parses a CSS-style id and class
|
(def ^{:doc "Regular expression that parses a CSS-style id and class
|
||||||
|
@ -120,141 +121,6 @@
|
||||||
(convert-custom-prop-value props)
|
(convert-custom-prop-value props)
|
||||||
(convert-prop-value props))))
|
(convert-prop-value props))))
|
||||||
|
|
||||||
;;; Specialization for input components
|
|
||||||
|
|
||||||
;; This gets set from reagent.dom
|
|
||||||
(defonce find-dom-node nil)
|
|
||||||
|
|
||||||
;; <input type="??" >
|
|
||||||
;; The properites 'selectionStart' and 'selectionEnd' only exist on some inputs
|
|
||||||
;; See: https://html.spec.whatwg.org/multipage/forms.html#do-not-apply
|
|
||||||
(def these-inputs-have-selection-api #{"text" "textarea" "password" "search"
|
|
||||||
"tel" "url"})
|
|
||||||
|
|
||||||
(defn ^boolean has-selection-api?
|
|
||||||
[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 ^clj component {:keys [on-write]}]
|
|
||||||
(if-not (and (identical? node (.-activeElement js/document))
|
|
||||||
(has-selection-api? (.-type node))
|
|
||||||
(string? rendered-value)
|
|
||||||
(string? dom-value))
|
|
||||||
;; just set the value, no need to worry about a cursor
|
|
||||||
(do
|
|
||||||
(set! (.-cljsDOMValue component) rendered-value)
|
|
||||||
(set! (.-value node) 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 (.-value node)]
|
|
||||||
(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)
|
|
||||||
(.-selectionStart node))
|
|
||||||
new-cursor-offset (- (count rendered-value)
|
|
||||||
existing-offset-from-end)]
|
|
||||||
(set! (.-cljsDOMValue component) rendered-value)
|
|
||||||
(set! (.-value node) rendered-value)
|
|
||||||
(when (fn? on-write)
|
|
||||||
(on-write rendered-value))
|
|
||||||
(set! (.-selectionStart node) new-cursor-offset)
|
|
||||||
(set! (.-selectionEnd node) new-cursor-offset))))))
|
|
||||||
|
|
||||||
(defn input-component-set-value [^clj this]
|
|
||||||
(when (.-cljsInputLive this)
|
|
||||||
(set! (.-cljsInputDirty this) false)
|
|
||||||
(let [rendered-value (.-cljsRenderedValue this)
|
|
||||||
dom-value (.-cljsDOMValue this)
|
|
||||||
;; 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 [^clj this on-change e]
|
|
||||||
(set! (.-cljsDOMValue this) (-> e .-target .-value))
|
|
||||||
;; Make sure the input is re-rendered, in case on-change
|
|
||||||
;; wants to keep the value unchanged
|
|
||||||
(when-not (.-cljsInputDirty this)
|
|
||||||
(set! (.-cljsInputDirty this) true)
|
|
||||||
(batch/do-after-render #(input-component-set-value this)))
|
|
||||||
(on-change e))
|
|
||||||
|
|
||||||
(defn input-render-setup
|
|
||||||
[^clj this ^js 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 (.-value jsprops)
|
|
||||||
value (if (nil? v) "" v)
|
|
||||||
on-change (.-onChange jsprops)]
|
|
||||||
(when-not (.-cljsInputLive this)
|
|
||||||
;; set initial value
|
|
||||||
(set! (.-cljsInputLive this) true)
|
|
||||||
(set! (.-cljsDOMValue this) value))
|
|
||||||
(set! (.-cljsRenderedValue this) value)
|
|
||||||
(js-delete jsprops "value")
|
|
||||||
(set! (.-defaultValue jsprops) value)
|
|
||||||
(set! (.-onChange jsprops) #(input-handle-change this on-change %)))))
|
|
||||||
|
|
||||||
(defn input-unmount [^clj this]
|
|
||||||
(set! (.-cljsInputLive this) nil))
|
|
||||||
|
|
||||||
(defn ^boolean input-component? [x]
|
|
||||||
(case x
|
|
||||||
("input" "textarea") true
|
|
||||||
false))
|
|
||||||
|
|
||||||
(def reagent-input-class nil)
|
|
||||||
|
|
||||||
(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 component jsprops first-child opts]
|
|
||||||
(let [this comp/*current-component*]
|
|
||||||
(input-render-setup this jsprops)
|
|
||||||
(make-element argv component jsprops first-child opts)))})
|
|
||||||
|
|
||||||
(defn reagent-input
|
|
||||||
[]
|
|
||||||
(when (nil? reagent-input-class)
|
|
||||||
(set! reagent-input-class (comp/create-class input-spec)))
|
|
||||||
reagent-input-class)
|
|
||||||
|
|
||||||
|
|
||||||
;;; Conversion from Hiccup forms
|
;;; Conversion from Hiccup forms
|
||||||
|
|
||||||
(deftype HiccupTag [tag id className custom])
|
(deftype HiccupTag [tag id className custom])
|
||||||
|
@ -329,8 +195,8 @@
|
||||||
jsprops (or (convert-props (if hasprops props) parsed)
|
jsprops (or (convert-props (if hasprops props) parsed)
|
||||||
#js {})
|
#js {})
|
||||||
first-child (+ first (if hasprops 1 0))]
|
first-child (+ first (if hasprops 1 0))]
|
||||||
(if (input-component? component)
|
(if (input/input-component? component)
|
||||||
(-> [(reagent-input) argv component jsprops first-child opts]
|
(-> [(input/reagent-input) argv component jsprops first-child opts]
|
||||||
(with-meta (meta argv))
|
(with-meta (meta argv))
|
||||||
(as-element opts))
|
(as-element opts))
|
||||||
(do
|
(do
|
||||||
|
@ -435,3 +301,5 @@
|
||||||
(.push a (as-element v opts)))
|
(.push a (as-element v opts)))
|
||||||
a)
|
a)
|
||||||
#js[component jsprops] argv))))
|
#js[component jsprops] argv))))
|
||||||
|
|
||||||
|
(set! input/make-element make-element)
|
||||||
|
|
Loading…
Reference in New Issue