Breaking change: Stop implementing IDeref in cloact components

Introduce set-state, replace-state and state functions instead.

Since we already have our own atom, it doesn't make any sense
to duplicate functionality. Also, the implementation was necessarily
very messy, and fragile.
This commit is contained in:
Dan Holmsand 2014-01-07 19:45:41 +01:00
parent 00317991ee
commit 53fef42768
4 changed files with 88 additions and 130 deletions

View File

@ -24,7 +24,7 @@ Cloact uses [Hiccup-like](https://github.com/weavejester/hiccup) markup instead
" text."]])
```
You use one component inside another:
You can use one component inside another:
```clj
(defn calling-component []
@ -47,63 +47,55 @@ You mount the component into the DOM like this:
```clj
(defn mountit []
(cloact/render-component [childcaller](.-body js/document)))
(cloact/render-component [childcaller]
(.-body js/document)))
```
assuming we have imported Cloact like this:
```clj
(ns readme
(:require [cloact.core :as cloact]))
(ns example
(:require [cloact.core :as cloact :refer [atom]]))
```
The state of the component is managed like a ClojureScript atom, so using it looks like this:
State is handled using Cloact's version of `atom`, like this:
```clj
(defn state-ful [props this]
;; "this" is the actual component
[:div {:on-click #(swap! this update-in [:clicked] inc)}
"I have been clicked "
(or (:clicked @this) "zero")
" times."])
```
State can also be handled using Cloact's version of atom, like this:
```clj
(def click-count (cloact/atom 0))
(def click-count (atom 0))
(defn state-ful-with-atom []
[:div {:on-click #(swap! click-count inc)}
"I have been clicked " @click-count " times."])
```
Any component that dereferences a cloact/atom will be automatically re-rendered.
Any component that dereferences a `cloact.core/atom` will be automatically re-rendered.
If you want do some setting up when the component is first created, the component function can return a new function that will be called to do the actual rendering:
```clj
(defn using-setup [props this]
(reset! this {:clicked 0})
(fn [props]
[:div {:on-click #(swap! this update-in [:clicked] inc)}
"I have been clicked " (:clicked @this) " times."]))
(defn timer-component []
(let [seconds-elapsed (atom 0)]
(fn []
(js/setTimeout #(swap! seconds-elapsed inc) 1000)
[:div
"Seconds Elapsed: " @seconds-elapsed])))
```
This way you can avoid using React's lifecycle callbacks like `getInitialState` and `componentWillMount` most of the time.
But you can still use them if you want to, either using `cloact/create-class` or by attaching meta-data to a component function:
But you can still use them if you want to, either using `cloact.core/create-class` or by attaching meta-data to a component function:
```clj
(def my-html (atom ""))
(defn plain-component [props this]
[:p "My html is " (:html @this)])
[:p "My html is " @my-html])
(def component-with-callback
(with-meta plain-component
{:component-did-mount
(fn [this]
(swap! this assoc :html
(.-innerHTML (cloact/dom-node this))))}))
(reset! my-html (.-innerHTML (cloact/dom-node this))))}))
```
See the examples directory for more examples.

View File

@ -10,9 +10,6 @@
(def React tmpl/React)
;; (defn as-component [comp]
;; (tmpl/as-component comp))
(defn render-component
([comp container]
(render-component comp container nil))
@ -42,15 +39,24 @@
(comp/replace-props comp props))
(defn state [this]
(comp/state this))
(defn props [comp]
(comp/get-props comp))
(defn replace-state [this new-state]
(comp/replace-state this new-state))
(defn children [comp]
(comp/get-children comp))
(defn set-state [this new-state]
(comp/set-state this new-state))
(defn dom-node [comp]
(.getDOMNode comp))
(defn props [this]
(comp/get-props this))
(defn children [this]
(comp/get-children this))
(defn dom-node [this]
(.getDOMNode this))

View File

@ -8,56 +8,32 @@
(def React tmpl/React)
;;; IDeref protocol as mixin
(def CloactMixin (js-obj))
(def -ToExtend (js-obj))
(set! (.-prototype -ToExtend) CloactMixin)
(extend-type -ToExtend
IEquiv
(-equiv [C other] (identical? C other))
IDeref
(-deref [C] (.-state C))
IMeta
(-meta [C] nil)
IPrintWithWriter
(-pr-writer [C writer opts]
(-write writer (str "#<" (-> C .-constructor .-displayName) ": "))
(pr-writer (.-state C) writer opts)
(-write writer ">"))
IWatchable
(-notify-watches [C old new]
(when-not (identical? old new)
(.forceUpdate C)))
(-add-watch [C key f] (assert false "Component isn't really watchable"))
(-remove-watch [C key] (assert false "Component isn't really watchable"))
IHash
(-hash [C] (goog/getUid C)))
(doseq [x (js-keys CloactMixin)]
;; Tell React to not autobind
(aset (aget CloactMixin x) "__reactDontBind" true))
;; Reference -ToExtend to show fucking google closure that it is used
(when-not -ToExtend
(.log js/console "this should never happen to " -ToExtend))
;;; Accessors
(defn replace-state [this new-state]
;; Don't use React's replaceState, since it doesn't play well
;; with clojure maps
(let [old-state (.-cljsState this)]
(when-not (identical? old-state new-state)
(set! (.-cljsState this) new-state)
(.forceUpdate this))))
(defn set-state [this new-state]
(replace-state this (merge (.-cljsState this) new-state)))
(defn state [this]
(.-cljsState this))
;; We store the "args" (i.e a vector like [comp props child1])
;; in .-cljsArgs, with optional args, which makes access a bit
;; in .-cljsArgs, with optional props, which makes access a bit
;; tricky. The upside is that we don't have to do any allocations.
(defn- args-of [C]
(defn args-of [C]
(-> C (aget "props") .-cljsArgs))
(defn- props-in-args [args]
(defn props-in-args [args]
(let [p (nth args 1 nil)]
(when (map? p) p)))
@ -71,7 +47,7 @@
(defn- cljs-props [C]
(-> C args-of props-in-args))
(defn- get-children [C]
(defn get-children [C]
(let [args (args-of C)
c (first-child args)]
(drop c args)))
@ -94,8 +70,8 @@
;;; Function wrapping
(defn- do-render [C f]
(let [res (f (cljs-props C) C (.-state C))
(defn do-render [C f]
(let [res (f (cljs-props C) C)
conv (if (vector? res)
(tmpl/as-component res)
(if (fn? res)
@ -103,7 +79,7 @@
res))]
conv))
(defn- render [C]
(defn render [C]
(assert C)
(when (nil? (.-cljsRatom C))
(set! (.-cljsRatom C)
@ -113,17 +89,15 @@
(do-render C (.-cljsRenderFn C)))))
(ratom/run (.-cljsRatom C)))
(defn- custom-wrapper [key f]
(defn custom-wrapper [key f]
(case key
:getDefaultProps
(assert false "getDefaultProps not supported yet")
:getInitialState
(fn [C]
;; reset! doesn't call -notifyWatches unless -watches is set
(set! (.-watches C) {})
(when f
(set! (.-state C) (merge (.-state C) (f C)))))
(set! (.-cljsState C) (merge (.-cljsState C) (f C)))))
:componentWillReceiveProps
(fn [C props]
@ -153,38 +127,30 @@
(render C))
nil))
(defn- default-wrapper [f]
(defn default-wrapper [f]
(if (fn? f)
(fn [& args]
(this-as C (apply f C args)))
f))
(defn- get-wrapper [key f name]
(defn get-wrapper [key f name]
(let [wrap (custom-wrapper key f)]
(when (and wrap f)
(assert (fn? f)
(str "Expected function in " name key " but got " f)))
(default-wrapper (or wrap f))))
(def obligatory {:getInitialState nil
:shouldComponentUpdate nil
(def obligatory {:shouldComponentUpdate nil
:componentWillUnmount nil})
(def aliases {:initialState :getInitialState
:defaultProps :getDefaultProps})
(defn- camelify-map-keys [m]
(defn camelify-map-keys [m]
(into {} (for [[k v] m]
[(-> k tmpl/dash-to-camel keyword) v])))
(defn- allow-aliases [m]
(into {} (for [[k v] m]
[(get aliases k k) v])))
(defn- add-obligatory [fun-map]
(defn add-obligatory [fun-map]
(merge obligatory fun-map))
(defn- wrap-funs [fun-map]
(defn wrap-funs [fun-map]
(let [name (or (:displayName fun-map)
(when-let [r (:render fun-map)]
(or (.-displayName r)
@ -193,17 +159,11 @@
(into {} (for [[k v] (assoc fun-map :displayName name1)]
[k (get-wrapper k v name1)]))))
(defn- add-atom-mixin
[props-map]
(merge-with concat props-map {:mixins [CloactMixin]}))
(defn- cljsify [body]
(defn cljsify [body]
(-> body
camelify-map-keys
allow-aliases
add-obligatory
wrap-funs
add-atom-mixin
clj->js))
(defn create-class

View File

@ -4,7 +4,7 @@
[cloact.ratom :refer [reaction]]
[cloact.debug :refer [dbg println log]])
(:require [cemerick.cljs.test :as t]
[cloact.core :as r :refer [atom]]
[cloact.core :as cloact :refer [atom]]
[cloact.ratom :as rv]))
(defn running [] (rv/running))
@ -22,8 +22,8 @@
(defn with-mounted-component [comp f]
(when isClient
(let [div (add-test-div "_testcloact")]
(let [comp (r/render-component comp div #(f comp div))]
(r/unmount-component-at-node div)))))
(let [comp (cloact/render-component comp div #(f comp div))]
(cloact/unmount-component-at-node div)))))
(defn found-in [re div]
(let [res (.-innerHTML div)]
@ -47,46 +47,46 @@
(deftest test-simple-callback
(when isClient
(let [ran (atom 0)
comp (r/create-class
{:component-did-mount #(swap! ran inc)
:render (fn [P C]
(assert (map? P))
(swap! ran inc)
[:div (str "hi " (:foo P) ".")])})]
comp (cloact/create-class
{:component-did-mount #(swap! ran inc)
:render (fn [P C]
(assert (map? P))
(swap! ran inc)
[:div (str "hi " (:foo P) ".")])})]
(with-mounted-component (comp {:foo "you"})
(fn [C div]
(swap! ran inc)
(is (found-in #"hi you" div))
(r/set-props C {:foo "there"})
(cloact/set-props C {:foo "there"})
(is (found-in #"hi there" div))
(let [runs @ran]
(r/set-props C {:foo "there"})
(cloact/set-props C {:foo "there"})
(is (found-in #"hi there" div))
(is (= runs @ran)))
(r/replace-props C {:foobar "not used"})
(cloact/replace-props C {:foobar "not used"})
(is (found-in #"hi ." div))))
(is (= 5 @ran)))))
(deftest test-state-change
(when isClient
(let [ran (atom 0)
comp (r/create-class
{:get-initial-state (fn [])
:render (fn [P C]
(swap! ran inc)
[:div (str "hi " (:foo @C))])})]
comp (cloact/create-class
{:get-initial-state (fn [])
:render (fn [P C]
(swap! ran inc)
[:div (str "hi " (:foo (cloact/state C)))])})]
(with-mounted-component (comp)
(fn [C div]
(swap! ran inc)
(is (found-in #"hi " div))
(swap! C assoc :foo "there")
(cloact/set-state C {:foo "there"})
(is (found-in #"hi there" div))
(swap! C assoc :foo "you")
(cloact/set-state C {:foo "you"})
(is (found-in #"hi you" div))))
(is (= 4 @ran)))))
@ -122,9 +122,9 @@
(let [ran (atom 0)
really-simple (fn [props this]
(swap! ran inc)
(swap! this assoc :foo "foobar")
(cloact/set-state this {:foo "foobar"})
(fn []
[:div (str "this is " (:foo @this))]))]
[:div (str "this is " (:foo (cloact/state this)))]))]
(with-mounted-component [really-simple nil nil]
(fn [c div]
(swap! ran inc)
@ -135,5 +135,5 @@
(let [comp (fn [props]
[:div (str "i am " (:foo props))])]
(is (re-find #"i am foobar"
(r/render-component-to-string
[comp {:foo "foobar"}])))))
(cloact/render-component-to-string
[comp {:foo "foobar"}])))))