diff --git a/src/test/frontend/react_impl.cljc b/src/test/frontend/react_impl.cljc new file mode 100644 index 0000000000..cf00a3dc9a --- /dev/null +++ b/src/test/frontend/react_impl.cljc @@ -0,0 +1,95 @@ +(ns frontend.react-impl + "Note: don't run component parallel" + ;;#?(:clj (:require [clojure.tools.macro :refer [name-with-attributes]])) + #?(:cljs (:require-macros [frontend.react-impl]))) + +(defn name-with-attributes + "copy: https://github.com/clojure/tools.macro/blob/master/src/main/clojure/clojure/tools/macro.clj#L282" + [name macro-args] + (let [[docstring macro-args] (if (string? (first macro-args)) + [(first macro-args) (next macro-args)] + [nil macro-args]) + [attr macro-args] (if (map? (first macro-args)) + [(first macro-args) (next macro-args)] + [{} macro-args]) + attr (if docstring + (assoc attr :doc docstring) + attr) + attr (if (meta name) + (conj (meta name) attr) + attr)] + [(with-meta name attr) macro-args])) + +#_{:component-key {:result nil + :watches [] + :f-path nil + :f nil}} +(def react-defines (atom {})) +(def ^:dynamic *f-indent* nil) +(def ^:dynamic *react-f-path* '()) +(def ^:dynamic *from-watching-fn* nil) + +(defn react' + [react-ref] + (let [ident *f-indent* + f (get-in @react-defines [ident :f])] + (cond + (= true *from-watching-fn*) + (deref react-ref) + + (ifn? f) + (let [component (get @react-defines ident)] + (when-not ((:watches component) react-ref) + (let [new-component (update component :watches conj react-ref)] + (add-watch react-ref react-ref + + (fn [& _] + (binding [*from-watching-fn* true] + (reset! (:result component) (f)) + (let [f-path (rest (get-in @react-defines [ident :f-path]))] + (doseq [{:keys [f ident]} f-path] + (binding [*f-indent* ident] + (let [component (get @react-defines ident)] + (reset! (:result component) (f))))))))) + (swap! react-defines assoc *f-indent* new-component) + @react-ref))) + + :else (deref react-ref)))) + +(defn react-fn + [f] + (let [result-ref (atom nil) + ident (keyword (gensym))] + (binding [*f-indent* ident + *react-f-path* (conj *react-f-path* {:f f :ident ident})] + (swap! react-defines assoc *f-indent* {:result result-ref + :watches #{} + :f-path *react-f-path* + :f f}) + (reset! result-ref (f)) + (deref (get-in @react-defines [ident :result]))))) + +#?(:clj (defmacro react + [react-ref] + `(react' ~react-ref))) + +#?(:clj (defmacro defc + [sym args & body] + `(defn ~sym ~args + (fn [] + (let [f# (fn [] ~@body)] + (react-fn f#)))))) + +#?(:clj (defmacro auto-clean-state + [& body] + `(do (reset! react-defines {}) + (let [result# ~@body] + (reset! react-defines {}) + result#)))) + +(comment + (def b (atom 1)) + + (defc inner + [c] + (+ c (react b)))) diff --git a/src/test/frontend/react_impl_test.cljs b/src/test/frontend/react_impl_test.cljs index 3a2d845549..9bd3edfbb8 100644 --- a/src/test/frontend/react_impl_test.cljs +++ b/src/test/frontend/react_impl_test.cljs @@ -1,60 +1,60 @@ (ns frontend.react-impl-test - "To facilitate testing, imitate the behavior of react") + "To facilitate testing, imitate the behavior of react" + (:require [frontend.react-impl :as r] + [cljs.test :refer [deftest is are testing use-fixtures]])) -;{:component-key {:result nil -; :watches []}} -(def react-defines (atom {})) -(def ^:dynamic *react-fn* nil) +(deftest simple-react-test + (r/auto-clean-state + (let [react-ref (atom 1)] -(defn react - [react-ref] - (let [f *react-fn*] - (cond - (= :from-watching-fn f) - @react-ref + (r/defc simple-component + [] + (+ 2 (r/react react-ref))) - (ifn? f) - (let [component (get @react-defines f)] - (when-not ((:watches component) react-ref) - (let [new-component (update component :watches conj react-ref)] - (add-watch react-ref react-ref - (fn [& _] - (binding [*react-fn* :from-watching-fn] - (reset! (:result component) (f))))) - (swap! react-defines assoc f new-component) - @react-ref))) + (let [get-result (simple-component)] - :else (deref react-ref)))) + (is (= 3 (get-result))) + (reset! react-ref 2) + (is (= 4 (get-result))))))) -(defn react-fn - [f] - {:pre [(fn? f)]} - (let [result-ref (atom nil) - ;; Each react-fn invoke will generate new *react-fn*, though the same f. - f' (fn [] (f))] - (binding [*react-fn* f'] - (swap! react-defines assoc f' {:result result-ref - :watches #{}}) - (reset! result-ref (f')) - {:clear-state-fn - (fn [] (-> (swap! react-defines dissoc f') - (empty?))) - :get-value-fn - (fn [] (deref (get-in @react-defines [f' :result])))}))) +(deftest nest-component-test + (r/auto-clean-state + (let [a (atom 1) + b (atom 2)] -(defn clear-react-resources - [] - (reset! react-defines nil)) + (r/defc inner + [] + (r/react b)) -(comment - (let [react-ref (atom 1) - f (fn [] - (let [haha (react react-ref)] - (* haha 2))) - {:keys [clear-state-fn get-value-fn]} (react-fn f)] - (prn (get-value-fn)) - (reset! react-ref 2) - (prn (get-value-fn)) - (clear-state-fn))) + (r/defc out + [] + (let [out (r/react a) + get-inner-result (inner)] + (+ out (get-inner-result)))) + (let [get-out-result (out)] + (is (= 3 (get-out-result))) + (reset! b 4) + (is (= 5 (get-out-result))))))) +(deftest defc-params-test + (r/auto-clean-state + (let [a (atom 1) + b (atom 2)] + + (r/defc inner-1 + [c] + (+ c (r/react b))) + + (r/defc out-1 + [] + (let [out (r/react a) + get-inner-result (inner-1 5)] + (+ out (get-inner-result)))) + + (let [get-out-result (out-1)] + (is (= 8 (get-out-result))) + + (reset! b 4) + + (is (= 10 (get-out-result))))))) \ No newline at end of file