Make functional Reagent components optional, using new opts parameter

Render/as-element and other functions now take additional parameter for
setting options.

Currently only option is :functional-reag-elements?,
if set to true, by default Reagent components are
created as Functions, which behave similarly to Classes,
but allow using Hooks.
This commit is contained in:
Juho Teperi 2020-03-28 15:49:03 +02:00
parent 63e118d2a0
commit 22a2841b45
8 changed files with 1236 additions and 1112 deletions

View File

@ -46,8 +46,8 @@
(defn as-element (defn as-element
"Turns a vector of Hiccup syntax into a React element. Returns form "Turns a vector of Hiccup syntax into a React element. Returns form
unchanged if it is not a vector." unchanged if it is not a vector."
[form] ([form] (tmpl/as-element form nil))
(tmpl/as-element form)) ([form opts] (tmpl/as-element form opts)))
(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
@ -60,9 +60,10 @@
"Returns an adapter for a Reagent component, that may be used from "Returns an adapter for a Reagent component, that may be used from
React, for example in JSX. A single argument, props, is passed to React, for example in JSX. A single argument, props, is passed to
the component, converted to a map." the component, converted to a map."
[c] ([c] (reactify-component c nil))
([c opts]
(assert-some c "Component") (assert-some c "Component")
(comp/reactify-component c)) (comp/reactify-component c opts)))
(defn render (defn render
"Render a Reagent component into the DOM. The first argument may be "Render a Reagent component into the DOM. The first argument may be

View File

@ -34,10 +34,13 @@
Returns the mounted component instance." Returns the mounted component instance."
([comp container] ([comp container]
(render comp container nil)) (render comp container nil))
([comp container callback] ([comp container callback-or-opts]
(ratom/flush!) (ratom/flush!)
(let [f (fn [] (let [[opts callback] (if (fn? callback-or-opts)
(tmpl/as-element (if (fn? comp) (comp) comp)))] [nil callback-or-opts]
[callback-or-opts (:callback callback-or-opts)])
f (fn []
(tmpl/as-element (if (fn? comp) (comp) comp) opts))]
(render-comp f container callback)))) (render-comp f container callback))))
(defn unmount-component-at-node (defn unmount-component-at-node

View File

@ -6,14 +6,18 @@
(defn render-to-string (defn render-to-string
"Turns a component into an HTML string." "Turns a component into an HTML string."
[component] ([component]
(render-to-string component nil))
([component opts]
(ratom/flush!) (ratom/flush!)
(binding [util/*non-reactive* true] (binding [util/*non-reactive* true]
(dom-server/renderToString (tmpl/as-element component)))) (dom-server/renderToString (tmpl/as-element component opts)))))
(defn render-to-static-markup (defn render-to-static-markup
"Turns a component into an HTML string, without data-react-id attributes, etc." "Turns a component into an HTML string, without data-react-id attributes, etc."
[component] ([component]
(render-to-static-markup component nil))
([component opts]
(ratom/flush!) (ratom/flush!)
(binding [util/*non-reactive* true] (binding [util/*non-reactive* true]
(dom-server/renderToStaticMarkup (tmpl/as-element component)))) (dom-server/renderToStaticMarkup (tmpl/as-element component opts)))))

View File

@ -111,10 +111,11 @@
5 (.call f c (nth v 1) (nth v 2) (nth v 3) (nth v 4)) 5 (.call f c (nth v 1) (nth v 2) (nth v 3) (nth v 4))
(.apply f c (.slice (into-array v) 1)))))] (.apply f c (.slice (into-array v) 1)))))]
(cond (cond
(vector? res) (as-element res) ;; FIXME: Opts
(vector? res) (as-element res nil)
(ifn? res) (let [f (if (reagent-class? res) (ifn? res) (let [f (if (reagent-class? res)
(fn [& args] (fn [& args]
(as-element (apply vector res args))) (as-element (apply vector res args) nil))
res)] res)]
(set! (.-reagentRender c) f) (set! (.-reagentRender c) f)
(recur c)) (recur c))
@ -373,7 +374,7 @@
cmp)) cmp))
(defn fn-to-class [f] (defn fn-to-class [f opts]
(assert-callable f) (assert-callable f)
(warn-unless (not (and (react-class? f) (warn-unless (not (and (react-class? f)
(not (reagent-class? f)))) (not (reagent-class? f))))
@ -389,15 +390,16 @@
res (create-class withrender)] res (create-class withrender)]
(cache-react-class f res)))) (cache-react-class f res))))
(defn as-class [tag] (defn as-class [tag opts]
;; TODO: Cache per opts
(if-some [cached-class (cached-react-class tag)] (if-some [cached-class (cached-react-class tag)]
cached-class cached-class
(fn-to-class tag))) (fn-to-class tag opts)))
(defn reactify-component [comp] (defn reactify-component [comp opts]
(if (react-class? comp) (if (react-class? comp)
comp comp
(as-class comp))) (as-class comp opts)))
(defn functional-wrap-render (defn functional-wrap-render
[c] [c]
@ -406,10 +408,10 @@
argv (.-argv c) argv (.-argv c)
res (apply f argv)] res (apply f argv)]
(cond (cond
(vector? res) (as-element res) (vector? res) (as-element res (.-opts c))
(ifn? res) (let [f (if (reagent-class? res) (ifn? res) (let [f (if (reagent-class? res)
(fn [& args] (fn [& args]
(as-element (apply vector res args))) (as-element (apply vector res args) (.-opts c)))
res)] res)]
(set! (.-reagentRender c) f) (set! (.-reagentRender c) f)
(recur c)) (recur c))
@ -501,6 +503,7 @@
original Reagent component." original Reagent component."
[tag] [tag]
;; TODO: Could be disabled for optimized builds? ;; TODO: Could be disabled for optimized builds?
;; TODO: Need to cache per opts?
(or (gobj/get fun-components tag) (or (gobj/get fun-components tag)
(let [f (fn [jsprops] (functional-render jsprops)) (let [f (fn [jsprops] (functional-render jsprops))
_ (set! (.-displayName f) (util/fun-name tag)) _ (set! (.-displayName f) (util/fun-name tag))

View File

@ -246,10 +246,10 @@
:component-did-update input-component-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 component jsprops first-child] (fn [argv component jsprops first-child opts]
(let [this comp/*current-component*] (let [this comp/*current-component*]
(input-render-setup this jsprops) (input-render-setup this jsprops)
(make-element argv component jsprops first-child)))}) (make-element argv component jsprops first-child opts)))})
(defn reagent-input (defn reagent-input
[] []
@ -293,13 +293,21 @@
(if (= :> (nth v 0 nil)) (if (= :> (nth v 0 nil))
(get-key (nth v 2 nil)))))) (get-key (nth v 2 nil))))))
(defn reag-element [tag v] (defn reag-element [tag v opts]
(let [c (comp/as-class tag opts)
jsprops #js {}]
(set! (.-argv jsprops) v)
(when-some [key (key-from-vec v)]
(set! (.-key jsprops) key))
(react/createElement c jsprops)))
(defn functional-reag-element [tag v opts]
(if (or (comp/react-class? tag) (if (or (comp/react-class? tag)
;; TODO: Should check others for real comptibility, this fixes tests ;; TODO: Should check others for real comptibility, this fixes tests
;; TODO: Drop support for fn + meta for Class component methods? ;; TODO: Drop support for fn + meta for Class component methods?
(:should-component-update (meta tag))) (:should-component-update (meta tag)))
;; as-class unncessary later as tag is always class ;; as-class unncessary later as tag is always class
(let [c (comp/as-class tag) (let [c (comp/as-class tag opts)
jsprops #js {}] jsprops #js {}]
(set! (.-argv jsprops) v) (set! (.-argv jsprops) v)
(when-some [key (key-from-vec v)] (when-some [key (key-from-vec v)]
@ -308,6 +316,7 @@
(let [jsprops #js {}] (let [jsprops #js {}]
(set! (.-reagentRender jsprops) tag) (set! (.-reagentRender jsprops) tag)
(set! (.-argv jsprops) (subvec v 1)) (set! (.-argv jsprops) (subvec v 1))
(set! (.-opts jsprops) opts)
(when-some [key (key-from-vec v)] (when-some [key (key-from-vec v)]
(set! (.-key jsprops) key)) (set! (.-key jsprops) key))
(react/createElement (comp/funtional-render-fn tag) jsprops)))) (react/createElement (comp/funtional-render-fn tag) jsprops))))
@ -335,7 +344,7 @@
(gobj/set tag-name-cache x v) (gobj/set tag-name-cache x v)
v))) v)))
(defn native-element [parsed argv first] (defn native-element [parsed argv first opts]
(let [component (.-tag parsed) (let [component (.-tag parsed)
props (nth argv first nil) props (nth argv first nil)
hasprops (or (nil? props) (map? props)) hasprops (or (nil? props) (map? props))
@ -343,13 +352,13 @@
#js {}) #js {})
first-child (+ first (if hasprops 1 0))] first-child (+ first (if hasprops 1 0))]
(if (input-component? component) (if (input-component? component)
(-> [(reagent-input) argv component jsprops first-child] (-> [(reagent-input) argv component jsprops first-child opts]
(with-meta (meta argv)) (with-meta (meta argv))
as-element) (as-element opts))
(do (do
(when-some [key (-> (meta argv) get-key)] (when-some [key (-> (meta argv) get-key)]
(set! (.-key jsprops) key)) (set! (.-key jsprops) key))
(make-element argv component jsprops first-child))))) (make-element argv component jsprops first-child opts)))))
(defn str-coll [coll] (defn str-coll [coll]
(if (dev?) (if (dev?)
@ -365,7 +374,7 @@
(defn hiccup-err [v & msg] (defn hiccup-err [v & msg]
(str (apply str msg) ": " (str-coll v) "\n" (comp/comp-name))) (str (apply str msg) ": " (str-coll v) "\n" (comp/comp-name)))
(defn vec-to-elem [v] (defn vec-to-elem [v opts]
(assert (pos? (count v)) (hiccup-err v "Hiccup form should not be empty")) (assert (pos? (count v)) (hiccup-err v "Hiccup form should not be empty"))
(let [tag (nth v 0 nil)] (let [tag (nth v 0 nil)]
(assert (valid-tag? tag) (hiccup-err v "Invalid Hiccup form")) (assert (valid-tag? tag) (hiccup-err v "Invalid Hiccup form"))
@ -377,53 +386,56 @@
(let [n (name tag) (let [n (name tag)
pos (.indexOf n ">")] pos (.indexOf n ">")]
(case pos (case pos
-1 (native-element (cached-parse n) v 1) -1 (native-element (cached-parse n) v 1 opts)
0 (let [component (nth v 1 nil)] 0 (let [component (nth v 1 nil)]
;; Support [:> component ...] ;; Support [:> component ...]
(assert (= ">" n) (hiccup-err v "Invalid Hiccup tag")) (assert (= ">" n) (hiccup-err v "Invalid Hiccup tag"))
(native-element (->HiccupTag component nil nil nil) v 2)) (native-element (->HiccupTag component nil nil nil) v 2 opts))
;; Support extended hiccup syntax, i.e :div.bar>a.foo ;; Support extended hiccup syntax, i.e :div.bar>a.foo
;; Apply metadata (e.g. :key) to the outermost element. ;; Apply metadata (e.g. :key) to the outermost element.
;; Metadata is probably used only with sequeneces, and in that case ;; Metadata is probably used only with sequeneces, and in that case
;; only the key of the outermost element matters. ;; only the key of the outermost element matters.
(recur (with-meta [(subs n 0 pos) (recur (with-meta [(subs n 0 pos)
(assoc (with-meta v nil) 0 (subs n (inc pos)))] (assoc (with-meta v nil) 0 (subs n (inc pos)))]
(meta v))))) (meta v))
opts)))
(instance? NativeWrapper tag) (instance? NativeWrapper tag)
(native-element tag v 1) (native-element tag v 1 opts)
:else (reag-element tag v)))) :else (if (:functional-reag-elements? opts)
(functional-reag-element tag v opts)
(reag-element tag v opts)))))
(declare expand-seq) (declare expand-seq)
(declare expand-seq-check) (declare expand-seq-check)
(defn as-element [x] (defn as-element [x opts]
(cond (js-val? x) x (cond (js-val? x) x
(vector? x) (vec-to-elem x) (vector? x) (vec-to-elem x opts)
(seq? x) (if (dev?) (seq? x) (if (dev?)
(expand-seq-check x) (expand-seq-check x opts)
(expand-seq x)) (expand-seq x opts))
(named? x) (name x) (named? x) (name x)
(satisfies? IPrintWithWriter x) (pr-str x) (satisfies? IPrintWithWriter x) (pr-str x)
:else x)) :else x))
(set! comp/as-element as-element) (set! comp/as-element as-element)
(defn expand-seq [s] (defn expand-seq [s opts]
(into-array (map as-element s))) (into-array (map as-element s)))
(defn expand-seq-dev [s ^clj o] (defn expand-seq-dev [s ^clj o opts]
(into-array (map (fn [val] (into-array (map (fn [val]
(when (and (vector? val) (when (and (vector? val)
(nil? (key-from-vec val))) (nil? (key-from-vec val)))
(set! (.-no-key o) true)) (set! (.-no-key o) true))
(as-element val)) (as-element val opts))
s))) s)))
(defn expand-seq-check [x] (defn expand-seq-check [x opts]
(let [ctx #js{} (let [ctx #js{}
[res derefed] (ratom/check-derefs #(expand-seq-dev x ctx))] [res derefed] (ratom/check-derefs #(expand-seq-dev x ctx opts))]
(when derefed (when derefed
(warn (hiccup-err x "Reactive deref not supported in lazy seq, " (warn (hiccup-err x "Reactive deref not supported in lazy seq, "
"it should be wrapped in doall"))) "it should be wrapped in doall")))
@ -431,17 +443,17 @@
(warn (hiccup-err x "Every element in a seq should have a unique :key"))) (warn (hiccup-err x "Every element in a seq should have a unique :key")))
res)) res))
(defn make-element [argv component jsprops first-child] (defn make-element [argv component jsprops first-child opts]
(case (- (count argv) first-child) (case (- (count argv) first-child)
;; Optimize cases of zero or one child ;; Optimize cases of zero or one child
0 (react/createElement component jsprops) 0 (react/createElement component jsprops)
1 (react/createElement component jsprops 1 (react/createElement component jsprops
(as-element (nth argv first-child nil))) (as-element (nth argv first-child nil) opts))
(.apply react/createElement nil (.apply react/createElement nil
(reduce-kv (fn [a k v] (reduce-kv (fn [a k v]
(when (>= k first-child) (when (>= k first-child)
(.push a (as-element v))) (.push a (as-element v opts)))
a) a)
#js[component jsprops] argv)))) #js[component jsprops] argv))))

View File

@ -10,14 +10,14 @@
(js/performance.mark "functional-start") (js/performance.mark "functional-start")
; (simple-benchmark [x [hello-world-component]] (tmpl/vec-to-elem x) 100000) ; (simple-benchmark [x [hello-world-component]] (tmpl/vec-to-elem x) 100000)
(dotimes [i 100000] (dotimes [i 100000]
(tmpl/vec-to-elem [hello-world-component])) (tmpl/vec-to-elem [hello-world-component] nil))
(js/performance.mark "functional-end") (js/performance.mark "functional-end")
(js/performance.measure "functional" "functional-start" "functional-end") (js/performance.measure "functional" "functional-start" "functional-end")
(js/performance.mark "class-start") (js/performance.mark "class-start")
; (simple-benchmark [x [^:class-component hello-world-component]] (tmpl/vec-to-elem x) 100000) ; (simple-benchmark [x [^:class-component hello-world-component]] (tmpl/vec-to-elem x) 100000)
(dotimes [i 100000] (dotimes [i 100000]
(tmpl/vec-to-elem [^:class-component hello-world-component])) (tmpl/vec-to-elem [^:class-component hello-world-component] nil))
(js/performance.mark "class-end") (js/performance.mark "class-end")
(js/performance.measure "class" "class-start" "class-end") (js/performance.measure "class" "class-start" "class-end")
) )

View File

@ -19,8 +19,8 @@
:after (fn [] :after (fn []
(set! rv/debug false))}) (set! rv/debug false))})
(defn rstr [react-elem] (defn rstr [react-elem opts]
(server/render-to-static-markup react-elem)) (server/render-to-static-markup react-elem opts))
(defn log-error [& f] (defn log-error [& f]
(debug/error (apply str f))) (debug/error (apply str f)))
@ -34,13 +34,21 @@
(finally (finally
(set! js/console.error org)))))) (set! js/console.error org))))))
;; Different set of options to try for most test cases
(def test-options
[nil
{:functional-reag-elements? true}] )
(deftest really-simple-test (deftest really-simple-test
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ran (r/atom 0) (let [ran (r/atom 0)
really-simple (fn [] really-simple (fn []
(swap! ran inc) (swap! ran inc)
[:div "div in really-simple"])] [:div "div in really-simple"])]
(with-mounted-component [really-simple nil nil] (with-mounted-component [really-simple nil nil]
opts
(fn [c div] (fn [c div]
(swap! ran inc) (swap! ran inc)
(is (= "div in really-simple" (.-innerText div))) (is (= "div in really-simple" (.-innerText div)))
@ -48,10 +56,11 @@
(is (= 2 @ran)) (is (= 2 @ran))
(rdom/force-update-all) (rdom/force-update-all)
(is (= 3 @ran)))) (is (= 3 @ran))))
(is (= 3 @ran))))) (is (= 3 @ran))))))
(deftest test-simple-callback (deftest test-simple-callback
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ran (r/atom 0) (let [ran (r/atom 0)
comp (r/create-class comp (r/create-class
{:component-did-mount #(swap! ran inc) {:component-did-mount #(swap! ran inc)
@ -65,13 +74,15 @@
(swap! ran inc) (swap! ran inc)
[:div (str "hi " (:foo props) ".")]))})] [:div (str "hi " (:foo props) ".")]))})]
(with-mounted-component [comp {:foo "you"} 1] (with-mounted-component [comp {:foo "you"} 1]
opts
(fn [C div] (fn [C div]
(swap! ran inc) (swap! ran inc)
(is (= "hi you." (.-innerText div))))) (is (= "hi you." (.-innerText div)))))
(is (= 3 @ran))))) (is (= 3 @ran))))))
(deftest test-state-change (deftest test-state-change
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ran (r/atom 0) (let [ran (r/atom 0)
self (r/atom nil) self (r/atom nil)
comp (r/create-class comp (r/create-class
@ -83,6 +94,7 @@
(swap! ran inc) (swap! ran inc)
[:div (str "hi " (:foo (r/state this)))]))})] [:div (str "hi " (:foo (r/state this)))]))})]
(with-mounted-component [comp] (with-mounted-component [comp]
opts
(fn [C div] (fn [C div]
(swap! ran inc) (swap! ran inc)
(is (= "hi initial" (.-innerText div))) (is (= "hi initial" (.-innerText div)))
@ -96,10 +108,11 @@
(r/set-state @self {:foo "you"}) (r/set-state @self {:foo "you"})
(r/flush) (r/flush)
(is (= "hi you" (.-innerText div))))) (is (= "hi you" (.-innerText div)))))
(is (= 4 @ran))))) (is (= 4 @ran))))))
(deftest test-ratom-change (deftest test-ratom-change
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ran (r/atom 0) (let [ran (r/atom 0)
runs (rv/running) runs (rv/running)
val (r/atom 0) val (r/atom 0)
@ -110,6 +123,7 @@
(swap! ran inc) (swap! ran inc)
[:div (str "val " @v1 " " @val " " @secval)])] [:div (str "val " @v1 " " @val " " @secval)])]
(with-mounted-component [comp] (with-mounted-component [comp]
opts
(fn [C div] (fn [C div]
(r/flush) (r/flush)
(is (not= runs (rv/running))) (is (not= runs (rv/running)))
@ -136,10 +150,11 @@
(is (= "val 1 1 0" (.-innerText div))) (is (= "val 1 1 0" (.-innerText div)))
(is (= 2 @ran) "did not run"))) (is (= 2 @ran) "did not run")))
(is (= runs (rv/running))) (is (= runs (rv/running)))
(is (= 2 @ran))))) (is (= 2 @ran))))))
(deftest batched-update-test [] (deftest batched-update-test []
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ran (r/atom 0) (let [ran (r/atom 0)
v1 (r/atom 0) v1 (r/atom 0)
v2 (r/atom 0) v2 (r/atom 0)
@ -152,6 +167,7 @@
[:div @v1 [:div @v1
[c2 {:val @v1}]])] [c2 {:val @v1}]])]
(with-mounted-component [c1] (with-mounted-component [c1]
opts
(fn [c div] (fn [c div]
(r/flush) (r/flush)
(is (= 2 @ran)) (is (= 2 @ran))
@ -172,10 +188,11 @@
; (swap! v2 inc) ; (swap! v2 inc)
; (r/flush) ; (r/flush)
; (is (= 9 @ran)) ; (is (= 9 @ran))
))))) ))))))
(deftest init-state-test (deftest init-state-test
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ran (r/atom 0) (let [ran (r/atom 0)
really-simple (fn [] really-simple (fn []
(let [this (r/current-component)] (let [this (r/current-component)]
@ -185,13 +202,15 @@
[:div (str "this is " [:div (str "this is "
(:foo (r/state this)))])))] (:foo (r/state this)))])))]
(with-mounted-component [really-simple nil nil] (with-mounted-component [really-simple nil nil]
opts
(fn [c div] (fn [c div]
(swap! ran inc) (swap! ran inc)
(is (= "this is foobar" (.-innerText div))))) (is (= "this is foobar" (.-innerText div)))))
(is (= 2 @ran))))) (is (= 2 @ran))))))
(deftest should-update-test (deftest should-update-test
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [parent-ran (r/atom 0) (let [parent-ran (r/atom 0)
child-ran (r/atom 0) child-ran (r/atom 0)
child-props (r/atom nil) child-props (r/atom nil)
@ -204,6 +223,7 @@
(swap! parent-ran inc) (swap! parent-ran inc)
[:div "child-foo" [child @child-props]])] [:div "child-foo" [child @child-props]])]
(with-mounted-component [parent nil nil] (with-mounted-component [parent nil nil]
opts
(fn [c div] (fn [c div]
(r/flush) (r/flush)
(is (= 1 @child-ran)) (is (= 1 @child-ran))
@ -250,10 +270,11 @@
(is (= 7 @child-ran)) (is (= 7 @child-ran))
(rdom/force-update-all) (rdom/force-update-all)
(is (= 8 @child-ran))))))) (is (= 8 @child-ran))))))))
(deftest dirty-test (deftest dirty-test
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ran (r/atom 0) (let [ran (r/atom 0)
state (r/atom 0) state (r/atom 0)
really-simple (fn [] really-simple (fn []
@ -262,6 +283,7 @@
(reset! state 3)) (reset! state 3))
[:div (str "state=" @state)])] [:div (str "state=" @state)])]
(with-mounted-component [really-simple nil nil] (with-mounted-component [really-simple nil nil]
opts
(fn [c div] (fn [c div]
(is (= 1 @ran)) (is (= 1 @ran))
(is (= "state=0" (.-innerText div))) (is (= "state=0" (.-innerText div)))
@ -269,21 +291,23 @@
(r/flush) (r/flush)
(is (= 2 @ran)) (is (= 2 @ran))
(is (= "state=3" (.-innerText div))))) (is (= "state=3" (.-innerText div)))))
(is (= 2 @ran))))) (is (= 2 @ran))))))
(defn as-string [comp] (defn as-string [comp opts]
(server/render-to-static-markup comp)) (server/render-to-static-markup comp opts))
(deftest to-string-test [] (deftest to-string-test []
(doseq [opts test-options]
(let [comp (fn [props] (let [comp (fn [props]
[:div (str "i am " (:foo props))])] [:div (str "i am " (:foo props))])]
(is (= "<div>i am foobar</div>" (as-string [comp {:foo "foobar"}]))))) (is (= "<div>i am foobar</div>" (as-string [comp {:foo "foobar"}] opts))))))
(deftest data-aria-test [] (deftest data-aria-test []
(doseq [opts test-options]
(is (= "<div data-foo=\"x\"></div>" (is (= "<div data-foo=\"x\"></div>"
(as-string [:div {:data-foo "x"}]))) (as-string [:div {:data-foo "x"}] opts)))
(is (= "<div aria-labelledby=\"x\"></div>" (is (= "<div aria-labelledby=\"x\"></div>"
(as-string [:div {:aria-labelledby "x"}]))) (as-string [:div {:aria-labelledby "x"}] opts)))
;; Skip test: produces warning in new React ;; Skip test: produces warning in new React
;; (is (not (re-find #"enctype" ;; (is (not (re-find #"enctype"
;; (as-string [:div {"enc-type" "x"}]))) ;; (as-string [:div {"enc-type" "x"}])))
@ -291,58 +315,61 @@
;; FIXME: For some reason UMD module returns everything in ;; FIXME: For some reason UMD module returns everything in
;; lowercase, and CommonJS with upper T ;; lowercase, and CommonJS with upper T
(is (re-find #"enc[tT]ype" (is (re-find #"enc[tT]ype"
(as-string [:div {"encType" "x"}])) (as-string [:div {"encType" "x"}] opts))
"Strings are passed through to React, and have to be camelcase.") "Strings are passed through to React, and have to be camelcase.")
(is (re-find #"enc[tT]ype" (is (re-find #"enc[tT]ype"
(as-string [:div {:enc-type "x"}])) (as-string [:div {:enc-type "x"}] opts))
"Strings are passed through to React, and have to be camelcase.")) "Strings are passed through to React, and have to be camelcase.")))
(deftest dynamic-id-class [] (deftest dynamic-id-class []
(doseq [opts test-options]
(is (re-find #"id=.foo" (is (re-find #"id=.foo"
(as-string [:div#foo {:class "bar"}]))) (as-string [:div#foo {:class "bar"}] opts)))
(is (= "<div class=\"foo bar\"></div>" (is (= "<div class=\"foo bar\"></div>"
(as-string [:div.foo {:class "bar"}]))) (as-string [:div.foo {:class "bar"}] opts)))
(is (= "<div class=\"foo bar\"></div>" (is (= "<div class=\"foo bar\"></div>"
(as-string [:div.foo.bar]))) (as-string [:div.foo.bar] opts)))
(is (= "<div class=\"foo bar\"></div>" (is (= "<div class=\"foo bar\"></div>"
(as-string [:div.foo {:className "bar"}]))) (as-string [:div.foo {:className "bar"}] opts)))
(is (= "<div class=\"foo bar\"></div>" (is (= "<div class=\"foo bar\"></div>"
(as-string [:div {:className "foo bar"}]))) (as-string [:div {:className "foo bar"}] opts)))
(is (re-find #"id=.foo" (is (re-find #"id=.foo"
(as-string [:div#foo.foo.bar]))) (as-string [:div#foo.foo.bar] opts)))
(is (re-find #"class=.xxx bar" (is (re-find #"class=.xxx bar"
(as-string [:div#foo.xxx.bar]))) (as-string [:div#foo.xxx.bar] opts)))
(is (re-find #"id=.foo" (is (re-find #"id=.foo"
(as-string [:div.bar {:id "foo"}]))) (as-string [:div.bar {:id "foo"}] opts)))
(is (re-find #"id=.foo" (is (re-find #"id=.foo"
(as-string [:div.bar.xxx {:id "foo"}]))) (as-string [:div.bar.xxx {:id "foo"}] opts)))
(is (= "<div id=\"foo\"></div>" (is (= "<div id=\"foo\"></div>"
(as-string [:div#bar {:id "foo"}])) (as-string [:div#bar {:id "foo"}] opts))
"Dynamic id overwrites static")) "Dynamic id overwrites static")))
(defmulti my-div :type) (defmulti my-div :type)
(defmethod my-div :fooish [child] [:div.foo (:content child)]) (defmethod my-div :fooish [child] [:div.foo (:content child)])
(defmethod my-div :barish [child] [:div.bar (:content child)]) (defmethod my-div :barish [child] [:div.bar (:content child)])
(deftest ifn-component [] (deftest ifn-component []
(doseq [opts test-options]
(let [comp {:foo [:div "foodiv"] (let [comp {:foo [:div "foodiv"]
:bar [:div "bardiv"]}] :bar [:div "bardiv"]}]
(is (= "<div><div>foodiv</div></div>" (is (= "<div><div>foodiv</div></div>"
(as-string [:div [comp :foo]]))) (as-string [:div [comp :foo]] opts)))
(is (= "<div><div>bardiv</div></div>" (is (= "<div><div>bardiv</div></div>"
(as-string [:div [comp :bar]]))) (as-string [:div [comp :bar]] opts)))
(is (= "<div class=\"foo\">inner</div>" (is (= "<div class=\"foo\">inner</div>"
(as-string [my-div {:type :fooish :content "inner"}]))))) (as-string [my-div {:type :fooish :content "inner"}] opts))))))
(deftest symbol-string-tag [] (deftest symbol-string-tag []
(is (= "<div>foobar</div>" (as-string ['div "foobar"]))) (doseq [opts test-options]
(is (= "<div>foobar</div>" (as-string ["div" "foobar"]))) (is (= "<div>foobar</div>" (as-string ['div "foobar"] opts)))
(is (= "<div id=\"foo\">x</div>" (as-string ['div#foo "x"]))) (is (= "<div>foobar</div>" (as-string ["div" "foobar"] opts)))
(is (= "<div id=\"foo\">x</div>" (as-string ["div#foo" "x"]))) (is (= "<div id=\"foo\">x</div>" (as-string ['div#foo "x"] opts)))
(is (= "<div class=\"foo bar\"></div>" (as-string ['div.foo {:class "bar"}]))) (is (= "<div id=\"foo\">x</div>" (as-string ["div#foo" "x"] opts)))
(is (= "<div class=\"foo bar\"></div>" (as-string ["div.foo.bar"]))) (is (= "<div class=\"foo bar\"></div>" (as-string ['div.foo {:class "bar"}] opts)))
(is (= "<div class=\"foo bar\"></div>" (as-string ["div.foo.bar"] opts)))
(is (re-find #"id=.foo" (is (re-find #"id=.foo"
(as-string ['div#foo.foo.bar])))) (as-string ['div#foo.foo.bar] opts)))))
(deftest partial-test [] (deftest partial-test []
(let [p1 (r/partial vector 1 2)] (let [p1 (r/partial vector 1 2)]
@ -354,32 +381,36 @@
(is (= (hash p1) (hash (r/partial vector 1 2)))))) (is (= (hash p1) (hash (r/partial vector 1 2))))))
(deftest test-null-component (deftest test-null-component
(doseq [opts test-options]
(let [null-comp (fn [do-show] (let [null-comp (fn [do-show]
(when do-show (when do-show
[:div "div in test-null-component"]))] [:div "div in test-null-component"]))]
(is (= "" (is (= ""
(as-string [null-comp false]))) (as-string [null-comp false] opts)))
(is (= "<div>div in test-null-component</div>" (is (= "<div>div in test-null-component</div>"
(as-string [null-comp true]))))) (as-string [null-comp true] opts))))))
(deftest test-string (deftest test-string
(doseq [opts test-options]
(is (= "<div data-reactroot=\"\">foo</div>" (is (= "<div data-reactroot=\"\">foo</div>"
(server/render-to-string [:div "foo"]))) (server/render-to-string [:div "foo"] opts)))
(is (= "<div data-reactroot=\"\"><p>foo</p></div>" (is (= "<div data-reactroot=\"\"><p>foo</p></div>"
(server/render-to-string [:div [:p "foo"]])))) (server/render-to-string [:div [:p "foo"]] opts)))))
(deftest test-static-markup (deftest test-static-markup
(doseq [opts test-options]
(is (= "<div>foo</div>" (is (= "<div>foo</div>"
(rstr [:div "foo"]))) (rstr [:div "foo"] opts)))
(is (= "<div class=\"bar\"><p>foo</p></div>" (is (= "<div class=\"bar\"><p>foo</p></div>"
(rstr [:div.bar [:p "foo"]]))) (rstr [:div.bar [:p "foo"]] opts)))
(is (= "<div class=\"bar\"><p>foobar</p></div>" (is (= "<div class=\"bar\"><p>foobar</p></div>"
(rstr [:div.bar {:dangerously-set-inner-HTML (rstr [:div.bar {:dangerously-set-inner-HTML
{:__html "<p>foobar</p>"}} ])))) {:__html "<p>foobar</p>"}}] opts)))))
(deftest test-return-class (deftest test-return-class
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ran (r/atom 0) (let [ran (r/atom 0)
top-ran (r/atom 0) top-ran (r/atom 0)
comp (fn [] comp (fn []
@ -398,6 +429,7 @@
prop (r/atom {:foo "you"}) prop (r/atom {:foo "you"})
parent (fn [] [comp @prop 1])] parent (fn [] [comp @prop 1])]
(with-mounted-component [parent] (with-mounted-component [parent]
opts
(fn [C div] (fn [C div]
(swap! ran inc) (swap! ran inc)
(is (= "hi you." (.-innerText div))) (is (= "hi you." (.-innerText div)))
@ -408,10 +440,11 @@
(r/flush) (r/flush)
(is (= "hi me." (.-innerText div))) (is (= "hi me." (.-innerText div)))
(is (= 1 @top-ran)) (is (= 1 @top-ran))
(is (= 4 @ran))))))) (is (= 4 @ran))))))))
(deftest test-return-class-fn (deftest test-return-class-fn
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ran (r/atom 0) (let [ran (r/atom 0)
top-ran (r/atom 0) top-ran (r/atom 0)
comp (fn [] comp (fn []
@ -426,6 +459,7 @@
prop (r/atom {:foo "you"}) prop (r/atom {:foo "you"})
parent (fn [] [comp @prop 1])] parent (fn [] [comp @prop 1])]
(with-mounted-component [parent] (with-mounted-component [parent]
opts
(fn [C div] (fn [C div]
(swap! ran inc) (swap! ran inc)
(is (= "hi you." (.-innerText div))) (is (= "hi you." (.-innerText div)))
@ -436,11 +470,13 @@
(r/flush) (r/flush)
(is (= "hi me." (.-innerText div))) (is (= "hi me." (.-innerText div)))
(is (= 1 @top-ran)) (is (= 1 @top-ran))
(is (= 4 @ran))))))) (is (= 4 @ran))))))))
(deftest test-create-element (deftest test-create-element
(doseq [opts test-options]
(let [ae r/as-element (let [ae r/as-element
ce r/create-element] ce r/create-element
rstr #(rstr % opts)]
(is (= (rstr (ce "div")) (is (= (rstr (ce "div"))
(rstr (ae [:div])))) (rstr (ae [:div]))))
(is (= (rstr (ce "div" nil)) (is (= (rstr (ce "div" nil))
@ -460,7 +496,7 @@
(is (= (rstr (ce "div" nil (ae [:div "foo"]))) (is (= (rstr (ce "div" nil (ae [:div "foo"])))
(rstr (ae [:div [:div "foo"]])))) (rstr (ae [:div [:div "foo"]]))))
(is (= (rstr (ae [:div (ce "div" nil "foo")])) (is (= (rstr (ae [:div (ce "div" nil "foo")]))
(rstr (ae [:div [:div "foo"]])))))) (rstr (ae [:div [:div "foo"]])))))))
(def ndiv (let [cmp (fn [])] (def ndiv (let [cmp (fn [])]
(gobj/extend (gobj/extend
@ -476,8 +512,10 @@
cmp)) cmp))
(deftest test-adapt-class (deftest test-adapt-class
(doseq [opts test-options]
(let [d1 (r/adapt-react-class ndiv) (let [d1 (r/adapt-react-class ndiv)
d2 (r/adapt-react-class "div")] d2 (r/adapt-react-class "div")
rstr #(rstr % opts)]
(is (= (rstr [:div]) (is (= (rstr [:div])
(rstr [d1]))) (rstr [d1])))
(is (= (rstr [:div "a"]) (is (= (rstr [:div "a"])
@ -498,11 +536,13 @@
(is (= (rstr [:div.foo "a"]) (is (= (rstr [:div.foo "a"])
(rstr [d2 {:class "foo"} "a"]))) (rstr [d2 {:class "foo"} "a"])))
(is (= (rstr [:div "a" "b" [:div "c"]]) (is (= (rstr [:div "a" "b" [:div "c"]])
(rstr [d2 "a" "b" [:div "c"]]))))) (rstr [d2 "a" "b" [:div "c"]]))))))
(deftest test-adapt-class-2 (deftest test-adapt-class-2
(doseq [opts test-options]
(let [d1 ndiv (let [d1 ndiv
d2 "div"] d2 "div"
rstr #(rstr % opts)]
(is (= (rstr [:div]) (is (= (rstr [:div])
(rstr [:> d1]))) (rstr [:> d1])))
(is (= (rstr [:div "a"]) (is (= (rstr [:div "a"])
@ -523,7 +563,7 @@
(is (= (rstr [:div.foo "a"]) (is (= (rstr [:div.foo "a"])
(rstr [:> d2 {:class "foo"} "a"]))) (rstr [:> d2 {:class "foo"} "a"])))
(is (= (rstr [:div "a" "b" [:div "c"]]) (is (= (rstr [:div "a" "b" [:div "c"]])
(rstr [:> d2 "a" "b" [:div "c"]]))))) (rstr [:> d2 "a" "b" [:div "c"]]))))))
(deftest adapt-react-class-shortcut-key-warning (deftest adapt-react-class-shortcut-key-warning
(let [w (debug/track-warnings (let [w (debug/track-warnings
@ -535,8 +575,10 @@
(is (empty? (:warn w))))) (is (empty? (:warn w)))))
(deftest test-reactize-component (deftest test-reactize-component
(doseq [opts test-options]
(let [ae r/as-element (let [ae r/as-element
ce r/create-element ce r/create-element
rstr #(rstr % opts)
a (atom nil) a (atom nil)
c1r (fn reactize [p & args] c1r (fn reactize [p & args]
(reset! a args) (reset! a args)
@ -558,9 +600,10 @@
(ae [:b "b"]) (ae [:b "b"])
(ae [:i "i"]))) (ae [:i "i"])))
(rstr [:p "p:a" [:b "b"] [:i "i"]]))) (rstr [:p "p:a" [:b "b"] [:i "i"]])))
(is (= nil @a)))) (is (= nil @a)))))
(deftest test-keys (deftest test-keys
(doseq [opts test-options]
(let [a nil ;; (r/atom "a") (let [a nil ;; (r/atom "a")
c (fn key-tester [] c (fn key-tester []
[:div [:div
@ -570,6 +613,7 @@
[:p {:key i} i (some-> a deref)])]) [:p {:key i} i (some-> a deref)])])
w (debug/track-warnings w (debug/track-warnings
#(with-mounted-component [c] #(with-mounted-component [c]
opts
(fn [c div])))] (fn [c div])))]
(is (empty? (:warn w)))) (is (empty? (:warn w))))
@ -583,12 +627,15 @@
w (debug/track-warnings w (debug/track-warnings
(wrap-capture-console-error (wrap-capture-console-error
#(with-mounted-component [c] #(with-mounted-component [c]
opts
(fn [c div]))))] (fn [c div]))))]
(if (dev?) (if (dev?)
(is (re-find #"Warning: Every element in a seq should have a unique :key: \(\[:button \{:on-click #object\[Function\]\}\] \[:button \{:on-click #object\[Function\]\}\] \[:button \{:on-click #object\[Function\]\}\]\)\n \(in reagenttest.testreagent.key_tester\)" (is (re-find #"Warning: Every element in a seq should have a unique :key: \(\[:button \{:on-click #object\[Function\]\}\] \[:button \{:on-click #object\[Function\]\}\] \[:button \{:on-click #object\[Function\]\}\]\)\n \(in reagenttest.testreagent.key_tester\)"
(first (:warn w))))))))) (first (:warn w))))))))))
(deftest test-extended-syntax (deftest test-extended-syntax
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(is (= "<p><b>foo</b></p>" (is (= "<p><b>foo</b></p>"
(rstr [:p>b "foo"]))) (rstr [:p>b "foo"])))
(is (= (rstr [:p.foo [:b "x"]]) (is (= (rstr [:p.foo [:b "x"]])
@ -598,7 +645,7 @@
(is (= (rstr [:div.foo [:p.bar.foo [:b.foobar "xy"]]]) (is (= (rstr [:div.foo [:p.bar.foo [:b.foobar "xy"]]])
(rstr [:div.foo>p.bar.foo>b.foobar {} "xy"]))) (rstr [:div.foo>p.bar.foo>b.foobar {} "xy"])))
(is (= (rstr [:div [:p.bar.foo [:a.foobar {:href "href"} "xy"]]]) (is (= (rstr [:div [:p.bar.foo [:a.foobar {:href "href"} "xy"]]])
(rstr [:div>p.bar.foo>a.foobar {:href "href"} "xy"])))) (rstr [:div>p.bar.foo>a.foobar {:href "href"} "xy"])))))
(deftest extended-syntax-metadata (deftest extended-syntax-metadata
(when r/is-client (when r/is-client
@ -612,6 +659,8 @@
))))) )))))
(deftest test-class-from-collection (deftest test-class-from-collection
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(is (= (rstr [:p {:class "a b c d"}]) (is (= (rstr [:p {:class "a b c d"}])
(rstr [:p {:class ["a" "b" "c" "d"]}]))) (rstr [:p {:class ["a" "b" "c" "d"]}])))
(is (= (rstr [:p {:class "a b c"}]) (is (= (rstr [:p {:class "a b c"}])
@ -619,9 +668,11 @@
(is (= (rstr [:p {:class "a b c"}]) (is (= (rstr [:p {:class "a b c"}])
(rstr [:p {:class '("a" "b" "c")}]))) (rstr [:p {:class '("a" "b" "c")}])))
(is (= (rstr [:p {:class "a b c"}]) (is (= (rstr [:p {:class "a b c"}])
(rstr [:p {:class #{"a" "b" "c"}}])))) (rstr [:p {:class #{"a" "b" "c"}}])))))
(deftest class-different-types (deftest class-different-types
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(testing "named values are supported" (testing "named values are supported"
(is (= (rstr [:p {:class "a"}]) (is (= (rstr [:p {:class "a"}])
(rstr [:p {:class :a}]))) (rstr [:p {:class :a}])))
@ -640,7 +691,7 @@
(testing "falsey values are filtered from collections" (testing "falsey values are filtered from collections"
(is (= (rstr [:p {:class "a b"}]) (is (= (rstr [:p {:class "a b"}])
(rstr [:p {:class [:a :b false nil]}])))) ) (rstr [:p {:class [:a :b false nil]}]))))))
(deftest test-force-update (deftest test-force-update
(let [v (atom {:v1 0 (let [v (atom {:v1 0
@ -693,13 +744,16 @@
(is (re-find #"atestcomponent" @a) "component-path should work"))))) (is (re-find #"atestcomponent" @a) "component-path should work")))))
(deftest test-sorted-map-key (deftest test-sorted-map-key
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(let [c1 (fn [map] (let [c1 (fn [map]
[:div (map 1)]) [:div (map 1)])
c2 (fn [] c2 (fn []
[c1 (sorted-map 1 "foo" 2 "bar")])] [c1 (sorted-map 1 "foo" 2 "bar")])]
(is (= "<div>foo</div>" (rstr [c2]))))) (is (= "<div>foo</div>" (rstr [c2]))))))
(deftest basic-with-let (deftest basic-with-let
(doseq [opts test-options]
(when r/is-client (when r/is-client
(let [n1 (atom 0) (let [n1 (atom 0)
n2 (atom 0) n2 (atom 0)
@ -712,16 +766,18 @@
(finally (finally
(swap! n3 inc))))] (swap! n3 inc))))]
(with-mounted-component [c] (with-mounted-component [c]
opts
(fn [_ div] (fn [_ div]
(is (= [1 1 0] [@n1 @n2 @n3])) (is (= [1 1 0] [@n1 @n2 @n3]))
(swap! val inc) (swap! val inc)
(is (= [1 1 0] [@n1 @n2 @n3])) (is (= [1 1 0] [@n1 @n2 @n3]))
(r/flush) (r/flush)
(is (= [1 2 0] [@n1 @n2 @n3])))) (is (= [1 2 0] [@n1 @n2 @n3]))))
(is (= [1 2 1] [@n1 @n2 @n3]))))) (is (= [1 2 1] [@n1 @n2 @n3]))))))
(deftest with-let-destroy-only (deftest with-let-destroy-only
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [n1 (atom 0) (let [n1 (atom 0)
n2 (atom 0) n2 (atom 0)
c (fn [] c (fn []
@ -731,12 +787,14 @@
(finally (finally
(swap! n2 inc))))] (swap! n2 inc))))]
(with-mounted-component [c] (with-mounted-component [c]
opts
(fn [_ div] (fn [_ div]
(is (= [1 0] [@n1 @n2])))) (is (= [1 0] [@n1 @n2]))))
(is (= [1 1] [@n1 @n2]))))) (is (= [1 1] [@n1 @n2]))))))
(deftest with-let-arg (deftest with-let-arg
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [a (atom 0) (let [a (atom 0)
s (r/atom "foo") s (r/atom "foo")
f (fn [x] f (fn [x]
@ -747,16 +805,19 @@
(r/with-let [] (r/with-let []
[f @s]))] [f @s]))]
(with-mounted-component [c] (with-mounted-component [c]
opts
(fn [_ div] (fn [_ div]
(is (= "foo" @a)) (is (= "foo" @a))
(reset! s "bar") (reset! s "bar")
(r/flush) (r/flush)
(is (= "bar" @a))))))) (is (= "bar" @a))))))))
(deftest with-let-non-reactive (deftest with-let-non-reactive
(doseq [opts test-options]
(let [n1 (atom 0) (let [n1 (atom 0)
n2 (atom 0) n2 (atom 0)
n3 (atom 0) n3 (atom 0)
rstr #(rstr % opts)
c (fn [] c (fn []
(r/with-let [a (swap! n1 inc)] (r/with-let [a (swap! n1 inc)]
(swap! n2 inc) (swap! n2 inc)
@ -764,9 +825,10 @@
(finally (finally
(swap! n3 inc))))] (swap! n3 inc))))]
(is (= (rstr [c]) (rstr [:div 1]))) (is (= (rstr [c]) (rstr [:div 1])))
(is (= [1 1 1] [@n1 @n2 @n3])))) (is (= [1 1 1] [@n1 @n2 @n3])))))
(deftest lifecycle (deftest lifecycle
(doseq [opts test-options]
(let [n1 (atom 0) (let [n1 (atom 0)
t (atom 0) t (atom 0)
res (atom {}) res (atom {})
@ -846,7 +908,7 @@
(is (= {:at 9 :args [@t [@comp "a" "b"] {:foo "bar"} nil]} (is (= {:at 9 :args [@t [@comp "a" "b"] {:foo "bar"} nil]}
(:did-update @res))))] (:did-update @res))))]
(when r/is-client (when r/is-client
(with-mounted-component [c2] check) (with-mounted-component [c2] opts check)
(is (= {:at 10 :args [@t]} (is (= {:at 10 :args [@t]}
(:will-unmount @res))) (:will-unmount @res)))
@ -855,10 +917,11 @@
(reset! n1 0) (reset! n1 0)
(with-mounted-component [c2] check) (with-mounted-component [c2] check)
(is (= {:at 10 :args [@t]} (is (= {:at 10 :args [@t]}
(:will-unmount @res)))))) (:will-unmount @res)))))))
(deftest lifecycle-native (deftest lifecycle-native
(doseq [opts test-options]
(let [n1 (atom 0) (let [n1 (atom 0)
t (atom 0) t (atom 0)
res (atom {}) res (atom {})
@ -958,9 +1021,9 @@
(is (= 9 at)) (is (= 9 at))
(is (= [@comp @oldprops] oldv))))] (is (= [@comp @oldprops] oldv))))]
(when r/is-client (when r/is-client
(with-mounted-component [cnative] check) (with-mounted-component [cnative] opts check)
(is (= {:at 10 :args [@t]} (is (= {:at 10 :args [@t]}
(:will-unmount @res)))))) (:will-unmount @res)))))))
(defn foo [] (defn foo []
[:div]) [:div])
@ -988,6 +1051,8 @@
(deftest test-err-messages (deftest test-err-messages
(when (dev?) (when (dev?)
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(is (thrown-with-msg? (is (thrown-with-msg?
:default #"Hiccup form should not be empty: \[]" :default #"Hiccup form should not be empty: \[]"
(rstr []))) (rstr [])))
@ -1035,7 +1100,7 @@
pkg "reagenttest.testreagent." pkg "reagenttest.testreagent."
stack1 (str "in " pkg "comp1") stack1 (str "in " pkg "comp1")
rend (fn [x] rend (fn [x]
(with-mounted-component x identity))] (with-mounted-component x opts identity))]
;; Error is orginally caused by comp1, so only that is shown in the error ;; Error is orginally caused by comp1, so only that is shown in the error
(let [e (debug/track-warnings (let [e (debug/track-warnings
@ -1056,7 +1121,7 @@
(is (re-find #"Error rendering component \(in reagenttest.testreagent.comp1\)" (is (re-find #"Error rendering component \(in reagenttest.testreagent.comp1\)"
(last (:error e))))) (last (:error e)))))
(let [e (debug/track-warnings #(r/as-element [nat]))] (let [e (debug/track-warnings #(r/as-element [nat] opts))]
(is (re-find #"Using native React classes directly" (is (re-find #"Using native React classes directly"
(-> e :warn first)))) (-> e :warn first))))
@ -1066,11 +1131,12 @@
(-> e :warn first)))) (-> e :warn first))))
(let [e (debug/track-warnings (let [e (debug/track-warnings
#(r/as-element (comp4)))] #(r/as-element (comp4) opts))]
(is (re-find #"Every element in a seq should have a unique :key" (is (re-find #"Every element in a seq should have a unique :key"
(-> e :warn first)))))))) (-> e :warn first)))))))))
(deftest test-error-boundary (deftest test-error-boundary
(doseq [opts test-options]
(let [error (r/atom nil) (let [error (r/atom nil)
info (r/atom nil) info (r/atom nil)
error-boundary (fn error-boundary [comp] error-boundary (fn error-boundary [comp]
@ -1092,6 +1158,7 @@
(wrap-capture-window-error (wrap-capture-window-error
(wrap-capture-console-error (wrap-capture-console-error
#(with-mounted-component [error-boundary [comp2]] #(with-mounted-component [error-boundary [comp2]]
opts
(fn [c div] (fn [c div]
(r/flush) (r/flush)
(is (= "Test error" (.-message @error))) (is (= "Test error" (.-message @error)))
@ -1102,9 +1169,10 @@
(is (re-find #"\n in reagenttest.testreagent.comp1 \(created by reagenttest.testreagent.comp2\)\n in reagenttest.testreagent.comp2 \(created by reagent[0-9]+\)\n in reagent[0-9]+ \(created by reagenttest.testreagent.error_boundary\)\n in reagenttest.testreagent.error_boundary" (is (re-find #"\n in reagenttest.testreagent.comp1 \(created by reagenttest.testreagent.comp2\)\n in reagenttest.testreagent.comp2 \(created by reagent[0-9]+\)\n in reagent[0-9]+ \(created by reagenttest.testreagent.error_boundary\)\n in reagenttest.testreagent.error_boundary"
(.-componentStack ^js @info))) (.-componentStack ^js @info)))
(is (re-find #"\n in .+\n in .+\n in reagent[0-9]+\n in .+" (is (re-find #"\n in .+\n in .+\n in reagent[0-9]+\n in .+"
(.-componentStack ^js @info))) )))))))) (.-componentStack ^js @info))))))))))))
(deftest test-dom-node (deftest test-dom-node
(doseq [opts test-options]
(let [node (atom nil) (let [node (atom nil)
ref (atom nil) ref (atom nil)
comp (r/create-class comp (r/create-class
@ -1114,22 +1182,28 @@
(fn [this] (fn [this]
(reset! node (rdom/dom-node this)))})] (reset! node (rdom/dom-node this)))})]
(with-mounted-component [comp] (with-mounted-component [comp]
opts
(fn [c div] (fn [c div]
(is (= "foobar" (.-innerHTML @ref))) (is (= "foobar" (.-innerHTML @ref)))
(is (= "foobar" (.-innerHTML @node))) (is (= "foobar" (.-innerHTML @node)))
(is (identical? @ref @node)))))) (is (identical? @ref @node)))))))
(deftest test-empty-input (deftest test-empty-input
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(is (= "<div><input/></div>" (is (= "<div><input/></div>"
(rstr [:div [:input]])))) (rstr [:div [:input]])))))
(deftest test-object-children (deftest test-object-children
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(is (= "<p>foo bar1</p>" (is (= "<p>foo bar1</p>"
(rstr [:p 'foo " " :bar nil 1]))) (rstr [:p 'foo " " :bar nil 1])))
(is (= "<p>#object[reagent.ratom.RAtom {:val 1}]</p>" (is (= "<p>#object[reagent.ratom.RAtom {:val 1}]</p>"
(rstr [:p (r/atom 1)])))) (rstr [:p (r/atom 1)])))))
(deftest test-after-render (deftest test-after-render
(doseq [opts test-options]
(let [spy (atom 0) (let [spy (atom 0)
val (atom 0) val (atom 0)
exp (atom 0) exp (atom 0)
@ -1145,6 +1219,7 @@
(is (= @exp @val)) (is (= @exp @val))
[:div {:ref #(reset! node %)} @state]))] [:div {:ref #(reset! node %)} @state]))]
(with-mounted-component [comp] (with-mounted-component [comp]
opts
(fn [c div] (fn [c div]
(is (= 1 @spy)) (is (= 1 @spy))
(swap! state inc) (swap! state inc)
@ -1163,20 +1238,26 @@
; (r/flush) ; (r/flush)
; (is (= 0 @spy)) ; (is (= 0 @spy))
)) ))
(is (= nil @node)))) (is (= nil @node)))))
(deftest style-property-names-are-camel-cased (deftest style-property-names-are-camel-cased
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(is (= "<div style=\"text-align:center\">foo</div>" (is (= "<div style=\"text-align:center\">foo</div>"
(rstr [:div {:style {:text-align "center"}} "foo"])))) (rstr [:div {:style {:text-align "center"}} "foo"])))))
(deftest custom-element-class-prop (deftest custom-element-class-prop
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(is (= "<custom-element class=\"foobar\">foo</custom-element>" (is (= "<custom-element class=\"foobar\">foo</custom-element>"
(rstr [:custom-element {:class "foobar"} "foo"]))) (rstr [:custom-element {:class "foobar"} "foo"])))
(is (= "<custom-element class=\"foobar\">foo</custom-element>" (is (= "<custom-element class=\"foobar\">foo</custom-element>"
(rstr [:custom-element.foobar "foo"])))) (rstr [:custom-element.foobar "foo"])))))
(deftest html-entities (deftest html-entities
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(testing "entity numbers can be unescaped always" (testing "entity numbers can be unescaped always"
(is (= "<i> </i>" (is (= "<i> </i>"
(rstr [:i (gstr/unescapeEntities "&#160;")])))) (rstr [:i (gstr/unescapeEntities "&#160;")]))))
@ -1184,7 +1265,7 @@
(when r/is-client (when r/is-client
(testing "When DOM is available, all named entities can be unescaped" (testing "When DOM is available, all named entities can be unescaped"
(is (= "<i> </i>" (is (= "<i> </i>"
(rstr [:i (gstr/unescapeEntities "&nbsp;")])))))) (rstr [:i (gstr/unescapeEntities "&nbsp;")])))))))
(defn context-wrapper [] (defn context-wrapper []
(r/create-class (r/create-class
@ -1214,12 +1295,13 @@
(deftest test-fragments (deftest test-fragments
(doseq [opts test-options]
(testing "Fragment as array" (testing "Fragment as array"
(let [comp (fn comp1 [] (let [comp (fn comp1 []
#js [(r/as-element [:div "hello"]) #js [(r/as-element [:div "hello"] opts)
(r/as-element [:div "world"])])] (r/as-element [:div "world"] opts)])]
(is (= "<div>hello</div><div>world</div>" (is (= "<div>hello</div><div>world</div>"
(as-string [comp]))))) (as-string [comp] opts)))))
(testing "Fragment element, :<>" (testing "Fragment element, :<>"
(let [comp (fn comp2 [] (let [comp (fn comp2 []
@ -1228,7 +1310,7 @@
[:div "world"] [:div "world"]
[:div "foo"] ])] [:div "foo"] ])]
(is (= "<div>hello</div><div>world</div><div>foo</div>" (is (= "<div>hello</div><div>world</div><div>foo</div>"
(as-string [comp]))))) (as-string [comp] opts)))))
(testing "Fragment key" (testing "Fragment key"
;; This would cause React warning if both fragements didn't have key set ;; This would cause React warning if both fragements didn't have key set
@ -1250,7 +1332,7 @@
[:div "1"] [:div "1"]
[:div "2"]])])] [:div "2"]])])]
(is (= "<div><div>hello</div><div>world</div><div>foo</div><div>1</div><div>2</div></div>" (is (= "<div><div>hello</div><div>world</div><div>foo</div><div>1</div><div>2</div></div>"
(as-string [comp])))))) (as-string [comp] opts)))))))
;; In bundle version, the names aren't optimized. ;; In bundle version, the names aren't optimized.
;; In node module processed versions, names probably are optimized. ;; In node module processed versions, names probably are optimized.
@ -1260,6 +1342,8 @@
(def Consumer (.-Consumer my-context)) (def Consumer (.-Consumer my-context))
(deftest new-context-test (deftest new-context-test
(doseq [opts test-options
:let [rstr #(rstr % opts)]]
(is (= "<div>Context: foo</div>" (is (= "<div>Context: foo</div>"
(rstr (r/create-element (rstr (r/create-element
Provider #js {:value "foo"} Provider #js {:value "foo"}
@ -1298,9 +1382,10 @@
(this-as this (this-as this
(r/as-element [:div "Context: " (.-context this)])))})] (r/as-element [:div "Context: " (.-context this)])))})]
(is (= "<div>Context: default</div>" (is (= "<div>Context: default</div>"
(rstr [comp])))))) (rstr [comp])))))))
(deftest on-failed-prop-comparison-in-should-update-swallow-exception-and-do-not-update-component (deftest on-failed-prop-comparison-in-should-update-swallow-exception-and-do-not-update-component
(doseq [opts test-options]
(let [prop (r/atom {:todos 1}) (let [prop (r/atom {:todos 1})
component-was-updated (atom false) component-was-updated (atom false)
error-thrown-after-updating-props (atom false) error-thrown-after-updating-props (atom false)
@ -1314,6 +1399,7 @@
(when (and r/is-client (dev?)) (when (and r/is-client (dev?))
(let [e (debug/track-warnings (let [e (debug/track-warnings
#(with-mounted-component [component] #(with-mounted-component [component]
opts
(fn [c div] (fn [c div]
(reset! prop (sorted-map 1 2)) (reset! prop (sorted-map 1 2))
(try (try
@ -1324,10 +1410,11 @@
(is (not @component-was-updated)) (is (not @component-was-updated))
(is (not @error-thrown-after-updating-props)))))] (is (not @error-thrown-after-updating-props)))))]
(is (re-find #"Warning: Exception thrown while comparing argv's in shouldComponentUpdate:" (is (re-find #"Warning: Exception thrown while comparing argv's in shouldComponentUpdate:"
(first (:warn e)))))))) (first (:warn e)))))))))
(deftest get-derived-state-from-props-test (deftest get-derived-state-from-props-test
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [prop (r/atom 0) (let [prop (r/atom 0)
;; Usually one can use Cljs object as React state. However, ;; Usually one can use Cljs object as React state. However,
;; getDerivedStateFromProps implementation in React uses ;; getDerivedStateFromProps implementation in React uses
@ -1344,14 +1431,16 @@
component (fn [] component (fn []
[pure-component {:value @prop}])] [pure-component {:value @prop}])]
(with-mounted-component [component] (with-mounted-component [component]
opts
(fn [c div] (fn [c div]
(is (= "Value foo" (.-innerText div))) (is (= "Value foo" (.-innerText div)))
(swap! prop inc) (swap! prop inc)
(r/flush) (r/flush)
(is (= "Value foo foo" (.-innerText div)))))))) (is (= "Value foo foo" (.-innerText div)))))))))
(deftest get-derived-state-from-error-test (deftest get-derived-state-from-error-test
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [prop (r/atom 0) (let [prop (r/atom 0)
component (r/create-class component (r/create-class
{:constructor (fn [this props] {:constructor (fn [this props]
@ -1370,14 +1459,16 @@
(wrap-capture-window-error (wrap-capture-window-error
(wrap-capture-console-error (wrap-capture-console-error
#(with-mounted-component [component [bad-component]] #(with-mounted-component [component [bad-component]]
opts
(fn [c div] (fn [c div]
(is (= "Ok" (.-innerText div))) (is (= "Ok" (.-innerText div)))
(swap! prop inc) (swap! prop inc)
(r/flush) (r/flush)
(is (= "Error" (.-innerText div)))))))))) (is (= "Error" (.-innerText div)))))))))))
(deftest get-snapshot-before-update-test (deftest get-snapshot-before-update-test
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [ref (react/createRef) (let [ref (react/createRef)
prop (r/atom 0) prop (r/atom 0)
did-update (atom nil) did-update (atom nil)
@ -1395,6 +1486,7 @@
component-2 (fn [] component-2 (fn []
[component {:value @prop}])] [component {:value @prop}])]
(with-mounted-component [component-2] (with-mounted-component [component-2]
opts
(fn [c div] (fn [c div]
;; Attach to DOM to get real height value ;; Attach to DOM to get real height value
(.appendChild js/document.body div) (.appendChild js/document.body div)
@ -1402,10 +1494,11 @@
(swap! prop inc) (swap! prop inc)
(r/flush) (r/flush)
(is (= {:height 20} @did-update)) (is (= {:height 20} @did-update))
(.removeChild js/document.body div)))))) (.removeChild js/document.body div)))))))
(deftest issue-462-test (deftest issue-462-test
(when r/is-client (when r/is-client
(doseq [opts test-options]
(let [val (r/atom 0) (let [val (r/atom 0)
render (atom 0) render (atom 0)
a (fn issue-462-a [nr] a (fn issue-462-a [nr]
@ -1419,6 +1512,7 @@
^{:key @val} ^{:key @val}
[b])] [b])]
(with-mounted-component [c] (with-mounted-component [c]
opts
(fn [c div] (fn [c div]
(is (= 1 @render)) (is (= 1 @render))
(reset! val 1) (reset! val 1)
@ -1426,13 +1520,14 @@
(is (= 2 @render)) (is (= 2 @render))
(reset! val 0) (reset! val 0)
(r/flush) (r/flush)
(is (= 3 @render))))))) (is (= 3 @render))))))))
(deftest functional-component-poc-simple (deftest functional-component-poc-simple
(when r/is-client (when r/is-client
(let [c (fn [x] (let [c (fn [x]
[:span "Hello " x])] [:span "Hello " x])]
(with-mounted-component [c "foo"] (with-mounted-component [c "foo"]
{:functional-reag-elements? true}
(fn [c div] (fn [c div]
(is (nil? c) "Render returns nil for stateless components") (is (nil? c) "Render returns nil for stateless components")
(is (= "Hello foo" (.-innerText div)))))))) (is (= "Hello foo" (.-innerText div))))))))
@ -1448,6 +1543,7 @@
(reset! set-count! set-count) (reset! set-count! set-count)
[:span "Count " c]))] [:span "Count " c]))]
(with-mounted-component [c 5] (with-mounted-component [c 5]
{:functional-reag-elements? true}
(fn [c div] (fn [c div]
(is (nil? c) "Render returns nil for stateless components") (is (nil? c) "Render returns nil for stateless components")
(is (= "Count 5" (.-innerText div))) (is (= "Count 5" (.-innerText div)))
@ -1460,6 +1556,7 @@
c (fn [x] c (fn [x]
[:span "Count " @count])] [:span "Count " @count])]
(with-mounted-component [c 5] (with-mounted-component [c 5]
{:functional-reag-elements? true}
(fn [c div] (fn [c div]
(is (nil? c) "Render returns nil for stateless components") (is (nil? c) "Render returns nil for stateless components")
(is (= "Count 5" (.-innerText div))) (is (= "Count 5" (.-innerText div)))
@ -1479,6 +1576,7 @@
(reset! set-count! set-count) (reset! set-count! set-count)
[:span "Counts " @r-count " " c]))] [:span "Counts " @r-count " " c]))]
(with-mounted-component [c 15] (with-mounted-component [c 15]
{:functional-reag-elements? true}
(fn [c div] (fn [c div]
(is (nil? c) "Render returns nil for stateless components") (is (nil? c) "Render returns nil for stateless components")
(is (= "Counts 3 15" (.-innerText div))) (is (= "Counts 3 15" (.-innerText div)))

View File

@ -2,15 +2,18 @@
(:require [reagent.core :as r] (:require [reagent.core :as r]
[reagent.dom :as rdom])) [reagent.dom :as rdom]))
(defn with-mounted-component [comp f] (defn with-mounted-component
([comp f]
(with-mounted-component comp nil f))
([comp opts f]
(when r/is-client (when r/is-client
(let [div (.createElement js/document "div")] (let [div (.createElement js/document "div")]
(try (try
(let [c (rdom/render comp div)] (let [c (rdom/render comp div opts)]
(f c div)) (f c div))
(finally (finally
(rdom/unmount-component-at-node div) (rdom/unmount-component-at-node div)
(r/flush)))))) (r/flush)))))))
(defn with-mounted-component-async [comp done f] (defn with-mounted-component-async [comp done f]
(when r/is-client (when r/is-client