diff --git a/src/reagent/impl/component.cljs b/src/reagent/impl/component.cljs index a4b1522..ffd6000 100644 --- a/src/reagent/impl/component.cljs +++ b/src/reagent/impl/component.cljs @@ -431,3 +431,64 @@ (if (react-class? comp) comp (as-class comp))) + +(defonce fun-component-state #js {}) + +(defn functional-render [jsprops] + (let [argv (.-argv jsprops) + tag (.-tag jsprops) + res (if util/*non-reactive* + (apply tag argv) + ;; Create persistent ID for each rendered functional component, + ;; this is used to store internal Reagent state, like render + ;; reaction etc. in a separate store where changes doesn't + ;; trigger render. + (let [[id _] (react/useState (js/Symbol)) + [_ update-count] (react/useState 0) + reagent-state (or (gobj/get fun-component-state id) + ;; TODO: Mock state atom? + (let [obj #js {:forceUpdate (fn [] (update-count inc)) + :cljsMountOrder (batch/next-mount-count)}] + (gobj/set fun-component-state id obj) + obj))] + + (react/useEffect + (fn mount [] + (fn unmount [] + (some-> (.-cljsRatom reagent-state) ratom/dispose!) + (gobj/remove fun-component-state id))) + ;; Only run effect once on mount and unmount + #js []) + + ;; Note: it might be possible to mock some React Component + ;; methods in the object and use it as *current-component* + + ;; TODO: If return value is ifn?, consider form-2 component. + + (assert-callable tag) + + (batch/mark-rendered reagent-state) + + ;; static-fns :render + (if-let [rat (.-cljsRatom reagent-state)] + (._run rat false) + (ratom/run-in-reaction + ;; Mock Class component API + #(binding [*current-component* reagent-state] + (apply tag argv)) + reagent-state + "cljsRatom" + batch/queue-render + rat-opts))))] + ;; do-render + ;; wrap-render + (cond + (vector? res) (as-element res) + ;; FIXME: Support form-2 components??? + ; (ifn? res) (let [f (if (reagent-class? res) + ; (create-class + ; {:reagent-render (fn [& args] + ; (as-element (apply vector res args)))}) + ; res)] + ; f) + :else res))) diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs index 9aba659..877d147 100644 --- a/src/reagent/impl/template.cljs +++ b/src/reagent/impl/template.cljs @@ -287,12 +287,27 @@ (-> v (nth 1 nil) get-key))) (defn reag-element [tag v] - (let [c (comp/as-class tag) - jsprops #js {}] - (set! (.-argv jsprops) v) - (when-some [key (key-from-vec v)] - (set! (.-key jsprops) key)) - (react/createElement c jsprops))) + (if (or false + (comp/react-class? tag) + ;; FIXME: Probably temporary workaround. + (:class-component (meta tag))) + ;; as-class unncessary later as tag is always class + (let [c (comp/as-class tag) + jsprops #js {}] + (set! (.-argv jsprops) v) + (when-some [key (key-from-vec v)] + (set! (.-key jsprops) key)) + (react/createElement c jsprops)) + (let [jsprops #js {:tag tag + :argv (subvec v 1)}] + (when-some [key (key-from-vec v)] + (set! (.-key jsprops) key)) + (react/createElement comp/functional-render jsprops)))) + +(defn reag-fun-element [v] + (let [tag (nth v 1) + argv (drop 2 v)] + (react/createElement comp/functional-render #js {:tag tag :argv argv}))) (defn fragment-element [argv] (let [props (nth argv 1 nil) @@ -355,6 +370,9 @@ (keyword-identical? :<> tag) (fragment-element v) + (keyword-identical? :< tag) + (reag-fun-element v) + (hiccup-tag? tag) (let [n (name tag) pos (.indexOf n ">")] diff --git a/test/reagenttest/runtests.cljs b/test/reagenttest/runtests.cljs index 5b9c904..9634238 100644 --- a/test/reagenttest/runtests.cljs +++ b/test/reagenttest/runtests.cljs @@ -6,6 +6,7 @@ [reagenttest.testtrack] [reagenttest.testwithlet] [reagenttest.testwrap] + [reagenttest.testnext] [reagent.impl.template-test] [reagent.impl.util-test] [clojure.test :as test] diff --git a/test/reagenttest/testreagent.cljs b/test/reagenttest/testreagent.cljs index ed5e5c1..525b90c 100644 --- a/test/reagenttest/testreagent.cljs +++ b/test/reagenttest/testreagent.cljs @@ -145,7 +145,7 @@ v2 (r/atom 0) c2 (fn [{val :val}] (swap! ran inc) - (is (= val @v1)) + (is (= @v1 val)) [:div @v2]) c1 (fn [] (swap! ran inc) @@ -175,13 +175,17 @@ (deftest init-state-test (when r/is-client (let [ran (r/atom 0) - really-simple (fn [] - (let [this (r/current-component)] - (swap! ran inc) - (r/set-state this {:foo "foobar"}) - (fn [] - [:div (str "this is " - (:foo (r/state this)))])))] + really-simple + ;; NOTE: Manually marking the following component as stateful. + ;; TODO: Should maybe just use create-class here. + ^:class-component + (fn [] + (let [this (r/current-component)] + (swap! ran inc) + (r/set-state this {:foo "foobar"}) + (fn [] + [:div (str "this is " + (:foo (r/state this)))])))] (with-mounted-component [really-simple nil nil] (fn [c div] (swap! ran inc) @@ -1124,6 +1128,7 @@ (r/flush) (is (= 1 @val)) (is (= 2 @spy)) + ;; TODO: Functional component can't be force updated from the outside (r/force-update c) (is (= 3 @spy)) (r/next-tick #(reset! spy 0)) @@ -1392,3 +1397,67 @@ (reset! val 0) (r/flush) (is (= 3 @render))))))) + +;; :< creates functional component for now. +;; This is for testing only, hopefully functional component +;; can be the default later. +(deftest functional-component-poc-simple + (when r/is-client + (let [c (fn [x] + [:span "Hello " x])] + (with-mounted-component [:< c "foo"] + (fn [c div] + (is (nil? c) "Render returns nil for stateless components") + (is (= "Hello foo" (.-innerText div)))))))) + +(deftest functional-component-poc-state-hook + (when r/is-client + (let [;; Probably not the best idea to keep + ;; refernce to state hook update fn, but + ;; works for testing. + set-count! (atom nil) + c (fn [x] + (let [[c set-count] (react/useState x)] + (reset! set-count! set-count) + [:span "Count " c]))] + (with-mounted-component [:< c 5] + (fn [c div] + (is (nil? c) "Render returns nil for stateless components") + (is (= "Count 5" (.-innerText div))) + (@set-count! 6) + (is (= "Count 6" (.-innerText div)))))))) + +(deftest functional-component-poc-ratom + (when r/is-client + (let [count (r/atom 5) + c (fn [x] + [:span "Count " @count])] + (with-mounted-component [:< c 5] + (fn [c div] + (is (nil? c) "Render returns nil for stateless components") + (is (= "Count 5" (.-innerText div))) + (reset! count 6) + (r/flush) + (is (= "Count 6" (.-innerText div))) + ;; TODO: Test that component RAtom is disposed + ))))) + + +(deftest functional-component-poc-ratom-state-hook + (when r/is-client + (let [r-count (r/atom 3) + set-count! (atom nil) + c (fn [x] + (let [[c set-count] (react/useState x)] + (reset! set-count! set-count) + [:span "Counts " @r-count " " c]))] + (with-mounted-component [:< c 15] + (fn [c div] + (is (nil? c) "Render returns nil for stateless components") + (is (= "Counts 3 15" (.-innerText div))) + (reset! r-count 6) + (r/flush) + (is (= "Counts 6 15" (.-innerText div))) + (@set-count! 17) + (is (= "Counts 6 17" (.-innerText div))) + )))))