diff --git a/src/reagent/dom.cljs b/src/reagent/dom.cljs
index 6827be8..08e215f 100644
--- a/src/reagent/dom.cljs
+++ b/src/reagent/dom.cljs
@@ -2,6 +2,7 @@
(:require [react-dom :as react-dom]
[reagent.impl.util :as util]
[reagent.impl.template :as tmpl]
+ [reagent.impl.input :as input]
[reagent.impl.batching :as batch]
[reagent.ratom :as ratom]))
@@ -53,7 +54,7 @@
[this]
(react-dom/findDOMNode this))
-(set! tmpl/find-dom-node dom-node)
+(set! input/find-dom-node dom-node)
(defn force-update-all
"Force re-rendering of all mounted Reagent components. This is
diff --git a/src/reagent/impl/input.cljs b/src/reagent/impl/input.cljs
new file mode 100644
index 0000000..40bf373
--- /dev/null
+++ b/src/reagent/impl/input.cljs
@@ -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)
+
+;;
+;; 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)
diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs
index 7509e9d..fba3447 100644
--- a/src/reagent/impl/template.cljs
+++ b/src/reagent/impl/template.cljs
@@ -5,11 +5,12 @@
[reagent.impl.util :as util :refer [named?]]
[reagent.impl.component :as comp]
[reagent.impl.batching :as batch]
+ [reagent.impl.input :as input]
[reagent.ratom :as ratom]
[reagent.debug :refer-macros [dev? warn]]
[goog.object :as gobj]))
-(declare as-element)
+(declare as-element make-element)
;; From Weavejester's Hiccup, via pump:
(def ^{:doc "Regular expression that parses a CSS-style id and class
@@ -120,141 +121,6 @@
(convert-custom-prop-value props)
(convert-prop-value props))))
-;;; Specialization for input components
-
-;; This gets set from reagent.dom
-(defonce find-dom-node nil)
-
-;;
-;; 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
(deftype HiccupTag [tag id className custom])
@@ -329,8 +195,8 @@
jsprops (or (convert-props (if hasprops props) parsed)
#js {})
first-child (+ first (if hasprops 1 0))]
- (if (input-component? component)
- (-> [(reagent-input) argv component jsprops first-child opts]
+ (if (input/input-component? component)
+ (-> [(input/reagent-input) argv component jsprops first-child opts]
(with-meta (meta argv))
(as-element opts))
(do
@@ -435,3 +301,5 @@
(.push a (as-element v opts)))
a)
#js[component jsprops] argv))))
+
+(set! input/make-element make-element)