288 lines
7.9 KiB
Clojure
Executable File
288 lines
7.9 KiB
Clojure
Executable File
(ns reagent.impl.component
|
|
(:require [reagent.impl.util :as util]
|
|
[reagent.impl.batching :as batch]
|
|
[reagent.ratom :as ratom]
|
|
[reagent.interop :refer-macros [.' .!]]
|
|
[reagent.debug :refer-macros [dbg prn dev? warn]]))
|
|
|
|
(declare ^:dynamic *current-component*)
|
|
|
|
(declare ^:dynamic *non-reactive*)
|
|
|
|
;;; State
|
|
|
|
(defn state-atom [this]
|
|
(let [sa (.' this :cljsState)]
|
|
(if-not (nil? sa)
|
|
sa
|
|
(.! this :cljsState (ratom/atom nil)))))
|
|
|
|
;; ugly circular dependency
|
|
(defn as-element [x]
|
|
(js/reagent.impl.template.as-element x))
|
|
|
|
;;; Rendering
|
|
|
|
(defn reagent-class? [c]
|
|
(and (fn? c)
|
|
(some? (.' c :cljsReactClass))))
|
|
|
|
(defn do-render-sub [c]
|
|
(let [f (.' c :cljsRender)
|
|
_ (assert (ifn? f))
|
|
p (.' c :props)
|
|
res (if (nil? (.' c :reagentRender))
|
|
(f c)
|
|
(let [argv (.' p :argv)
|
|
n (count argv)]
|
|
(case n
|
|
1 (f)
|
|
2 (f (nth argv 1))
|
|
3 (f (nth argv 1) (nth argv 2))
|
|
4 (f (nth argv 1) (nth argv 2) (nth argv 3))
|
|
5 (f (nth argv 1) (nth argv 2) (nth argv 3) (nth argv 4))
|
|
(apply f (subvec argv 1)))))]
|
|
(if (vector? res)
|
|
(as-element res)
|
|
(if (ifn? res)
|
|
(let [f (if (reagent-class? res)
|
|
(fn [& args]
|
|
(as-element (apply vector res args)))
|
|
res)]
|
|
(.! c :cljsRender f)
|
|
(recur c))
|
|
res))))
|
|
|
|
(declare comp-name)
|
|
|
|
(defn do-render [c]
|
|
(binding [*current-component* c]
|
|
(if (dev?)
|
|
;; Log errors, without using try/catch (and mess up call stack)
|
|
(let [ok (array false)]
|
|
(try
|
|
(let [res (do-render-sub c)]
|
|
(aset ok 0 true)
|
|
res)
|
|
(finally
|
|
(when-not (aget ok 0)
|
|
(js/console.error (str "Error rendering component "
|
|
(comp-name)))))))
|
|
(do-render-sub c))))
|
|
|
|
|
|
;;; Method wrapping
|
|
|
|
(def static-fns {:render
|
|
(fn []
|
|
(this-as c
|
|
(if-not *non-reactive*
|
|
(batch/run-reactively c #(do-render c))
|
|
(do-render c))))})
|
|
|
|
(defn custom-wrapper [key f]
|
|
(case key
|
|
:getDefaultProps
|
|
(assert false "getDefaultProps not supported yet")
|
|
|
|
:getInitialState
|
|
(fn []
|
|
(this-as c
|
|
(reset! (state-atom c) (f c))))
|
|
|
|
:componentWillReceiveProps
|
|
(fn [props]
|
|
(this-as c
|
|
(f c (.' props :argv))))
|
|
|
|
:shouldComponentUpdate
|
|
(fn [nextprops nextstate]
|
|
(or util/*always-update*
|
|
(this-as c
|
|
;; Don't care about nextstate here, we use forceUpdate
|
|
;; when only when state has changed anyway.
|
|
(let [old-argv (.' c :props.argv)
|
|
new-argv (.' nextprops :argv)]
|
|
(if (nil? f)
|
|
(or (nil? old-argv)
|
|
(nil? new-argv)
|
|
(not= old-argv new-argv))
|
|
(f c old-argv new-argv))))))
|
|
|
|
:componentWillUpdate
|
|
(fn [nextprops]
|
|
(this-as c
|
|
(f c (.' nextprops :argv))))
|
|
|
|
:componentDidUpdate
|
|
(fn [oldprops]
|
|
(this-as c
|
|
(f c (.' oldprops :argv))))
|
|
|
|
:componentWillMount
|
|
(fn []
|
|
(this-as c
|
|
(.! c :cljsMountOrder (batch/next-mount-count))
|
|
(when-not (nil? f)
|
|
(f c))))
|
|
|
|
:componentWillUnmount
|
|
(fn []
|
|
(this-as c
|
|
(batch/dispose c)
|
|
(when-not (nil? f)
|
|
(f c))))
|
|
|
|
nil))
|
|
|
|
(defn default-wrapper [f]
|
|
(if (ifn? f)
|
|
(fn [& args]
|
|
(this-as c (apply f c args)))
|
|
f))
|
|
|
|
(def dont-wrap #{:cljsRender :render :reagentRender :cljsName})
|
|
|
|
(defn dont-bind [f]
|
|
(if (fn? f)
|
|
(doto f
|
|
(.! :__reactDontBind true))
|
|
f))
|
|
|
|
(defn get-wrapper [key f name]
|
|
(if (dont-wrap key)
|
|
(dont-bind f)
|
|
(let [wrap (custom-wrapper key f)]
|
|
(when (and wrap f)
|
|
(assert (ifn? f)
|
|
(str "Expected function in " name key " but got " f)))
|
|
(or wrap (default-wrapper f)))))
|
|
|
|
(def obligatory {:shouldComponentUpdate nil
|
|
:componentWillMount nil
|
|
:componentWillUnmount nil})
|
|
|
|
(def dash-to-camel (util/memoize-1 util/dash-to-camel))
|
|
|
|
(defn camelify-map-keys [fun-map]
|
|
(reduce-kv (fn [m k v]
|
|
(assoc m (-> k dash-to-camel keyword) v))
|
|
{} fun-map))
|
|
|
|
(defn add-obligatory [fun-map]
|
|
(merge obligatory fun-map))
|
|
|
|
(defn add-render [fun-map render-f name]
|
|
(let [fm (assoc fun-map
|
|
:cljsRender render-f
|
|
:render (:render static-fns))]
|
|
(if (dev?)
|
|
(assoc fm :cljsName (fn [] name))
|
|
fm)))
|
|
|
|
(defn fun-name [f]
|
|
(or (and (fn? f)
|
|
(or (.' f :displayName)
|
|
(.' f :name)))
|
|
(and (implements? INamed f)
|
|
(name f))
|
|
(let [m (meta f)]
|
|
(if (map? m)
|
|
(:name m)))))
|
|
|
|
(defn wrap-funs [fmap]
|
|
(let [fun-map (if-some [cf (:componentFunction fmap)]
|
|
(-> fmap
|
|
(assoc :reagentRender cf)
|
|
(dissoc :componentFunction))
|
|
fmap)
|
|
render-fun (or (:reagentRender fun-map)
|
|
(:render fun-map))
|
|
_ (assert (ifn? render-fun)
|
|
(str "Render must be a function, not "
|
|
(pr-str render-fun)))
|
|
name (str (or (:displayName fun-map)
|
|
(fun-name render-fun)))
|
|
name' (if (empty? name)
|
|
(str (gensym "reagent"))
|
|
(clojure.string/replace name #"\$" "."))
|
|
fmap (-> fun-map
|
|
(assoc :displayName name')
|
|
(add-render render-fun name'))]
|
|
(reduce-kv (fn [m k v]
|
|
(assoc m k (get-wrapper k v name')))
|
|
{} fmap)))
|
|
|
|
(defn map-to-js [m]
|
|
(reduce-kv (fn [o k v]
|
|
(doto o
|
|
(aset (name k) v)))
|
|
#js{} m))
|
|
|
|
(defn cljsify [body]
|
|
(-> body
|
|
camelify-map-keys
|
|
add-obligatory
|
|
wrap-funs
|
|
map-to-js))
|
|
|
|
(defn create-class
|
|
[body]
|
|
(assert (map? body))
|
|
(let [spec (cljsify body)
|
|
res (.' js/React createClass spec)
|
|
f (fn [& args]
|
|
(warn "Calling the result of create-class as a function is "
|
|
"deprecated in " (.' res :displayName) ". Use a vector "
|
|
"instead.")
|
|
(as-element (apply vector res args)))]
|
|
(util/cache-react-class f res)
|
|
(util/cache-react-class res res)
|
|
f))
|
|
|
|
(defn component-path [c]
|
|
(let [elem (some-> (or (some-> c
|
|
(.' :_reactInternalInstance))
|
|
c)
|
|
(.' :_currentElement))
|
|
name (some-> elem
|
|
(.' :type)
|
|
(.' :displayName))
|
|
path (some-> elem
|
|
(.' :_owner)
|
|
component-path
|
|
(str " > "))
|
|
res (str path name)]
|
|
(when-not (empty? res) res)))
|
|
|
|
(defn comp-name []
|
|
(if (dev?)
|
|
(let [c *current-component*
|
|
n (or (component-path c)
|
|
(some-> c (.' cljsName)))]
|
|
(if-not (empty? n)
|
|
(str " (in " n ")")
|
|
""))
|
|
""))
|
|
|
|
(defn shallow-obj-to-map [o]
|
|
(into {} (for [k (js-keys o)]
|
|
[(keyword k) (aget o k)])))
|
|
|
|
(def elem-counter 0)
|
|
|
|
(defn reactify-component [comp]
|
|
(.' js/React createClass
|
|
#js{:displayName "react-wrapper"
|
|
:render
|
|
(fn []
|
|
(this-as this
|
|
(as-element
|
|
[comp
|
|
(-> (.' this :props)
|
|
shallow-obj-to-map
|
|
;; ensure re-render, might get mutable js data
|
|
(assoc :-elem-count
|
|
(set! elem-counter
|
|
(inc elem-counter))))])))}))
|