Breaking change: Allow arbitrary arguments to component functions

Previously, the first argument had to be a map, and the rest of the
arguments where passed as a vector as the second argument.

Now [my-comp foo...] generally behaves as (my-comp foo...).

Since `this` is no longer passed to component functions, add
current-component function.

Also change signatures of React callbacks, to reflect that arbitrary
arguments can be passed.

Remove set-props and replace-props for the same reason.

Add argv accessor and set-args instead.
This commit is contained in:
Dan Holmsand 2014-02-08 13:55:01 +01:00
parent 69384d98c6
commit 3c2c63402e
6 changed files with 132 additions and 110 deletions

View File

@ -1,5 +1,5 @@
(defproject reagent "0.3.0"
(defproject reagent "0.4.0-SNAPSHOT"
:url "http://github.com/holmsand/reagent"
:license {:name "MIT"}
:description "A simple ClojureScript interface to React"

View File

@ -41,14 +41,14 @@ Returns the mounted component instance."
"Create a component, React style. Should be called with a map,
looking like this:
{:get-initial-state (fn [this])
:component-will-receive-props (fn [this new-props])
:should-component-update (fn [this old-props new-props old-children new-children])
:component-will-receive-props (fn [this new-argv])
:should-component-update (fn [this old-argv new-argv])
:component-will-mount (fn [this])
:component-did-mount (fn [this])
:component-will-update (fn [this new-props new-children])
:component-did-update (fn [this old-props old-children])
:component-will-update (fn [this new-argv])
:component-did-update (fn [this old-argv])
:component-will-unmount (fn [this])
:render (fn [props children this])}
:render (fn [this])}
Everything is optional, except :render.
"
@ -56,16 +56,12 @@ Everything is optional, except :render.
(comp/create-class spec))
(defn replace-args [comp new-args]
(assert (vector? new-args))
(comp/set-args comp new-args))
(defn set-props
"Merge the props of a mounted, top-level component."
[comp props]
(comp/set-props comp props))
(defn replace-props
"Set the props of a mounted, top-level component."
[comp props]
(comp/replace-props comp props))
(defn current-component []
comp/*current-component*)
(defn state
@ -94,6 +90,11 @@ Everything is optional, except :render.
[this]
(comp/get-children this))
(defn argv
"Returns the entire Hiccup form passed to the component."
[this]
(comp/get-argv this))
(defn dom-node
"Returns the root DOM node of a mounted component."
[this]

View File

@ -2,11 +2,12 @@
(ns reagent.impl.component
(:refer-clojure :exclude [flush])
(:require [reagent.impl.template :as tmpl
:refer [cljs-props cljs-children cljs-level React]]
:refer [cljs-argv cljs-level React]]
[reagent.impl.util :as util]
[reagent.ratom :as ratom]
[reagent.debug :refer-macros [dbg prn]]))
(declare ^:dynamic *current-component*)
(def cljs-state "cljsState")
@ -29,20 +30,29 @@
(defn js-props [C]
(aget C "props"))
(defn props-in-props [props]
(aget props cljs-props))
(defn extract-props [v]
(let [p (get v 1)]
(if (map? p) p)))
(defn extract-children [v]
(let [first-child (if (-> v (get 1) map?) 2 1)]
(if (> (count v) first-child)
(subvec v first-child))))
(defn get-argv [C]
(-> C js-props (aget cljs-argv)))
(defn get-props [C]
(-> C js-props props-in-props))
(-> C get-argv extract-props))
(defn get-children [C]
(-> C js-props (aget cljs-children)))
(-> C get-argv extract-children))
(defn replace-props [C newprops]
(.setProps C (js-obj cljs-props newprops)))
(defn set-args [C new-args]
(let [argv (get-argv C)]
(.setProps C (js-obj cljs-argv
(into [(argv 0)] new-args)))))
(defn set-props [C newprops]
(replace-props C (merge (get-props C) newprops)))
;;; Rendering
@ -98,17 +108,25 @@
(defn do-render [C f]
(set! (.-cljsIsDirty C) false)
(let [p (js-props C)
props (props-in-props p)
children (aget p cljs-children)
;; Call render function with props, children, component
res (f props children C)
conv (if (vector? res)
(tmpl/as-component res (aget p cljs-level))
(if (fn? res)
(do-render C (set! (.-cljsRenderFn C) res))
res))]
conv))
(binding [*current-component* C]
(let [p (js-props C)
argv (aget p cljs-argv)
n (count argv)
res (if (nil? (aget C "componentFunction"))
(f C)
(case n
1 (f)
2 (f (argv 1))
3 (f (argv 1) (argv 2))
4 (f (argv 1) (argv 2) (argv 3))
5 (f (argv 1) (argv 2) (argv 3) (argv 4))
(apply f (subvec argv 1))))
conv (if (vector? res)
(tmpl/as-component res (aget p cljs-level))
(if (fn? res)
(do-render C (set! (.-cljsRenderFn C) res))
res))]
conv)))
(defn render [C]
(assert C)
@ -136,33 +154,28 @@
:componentWillReceiveProps
(fn [C props]
(when f (f C (props-in-props props))))
(when f (f C (aget props cljs-argv))))
:shouldComponentUpdate
(fn [C nextprops nextstate]
;; Don't care about nextstate here, we use forceUpdate
;; when only when state has changed anyway.
(let [inprops (js-props C)
p1 (aget inprops cljs-props)
c1 (aget inprops cljs-children)
p2 (aget nextprops cljs-props)
c2 (aget nextprops cljs-children)]
old-argv (aget inprops cljs-argv)
new-argv (aget nextprops cljs-argv)]
(if (nil? f)
(not (util/equal-args p1 c1 p2 c2))
;; call f with oldprops newprops oldchildren newchildren
(f C p1 p2 c1 c2))))
(not (util/equal-args old-argv new-argv))
(f C old-argv new-argv))))
:componentWillUpdate
(fn [C nextprops]
(let [p (aget nextprops cljs-props)
c (aget nextprops cljs-children)]
(f C p c)))
(let [next-argv (aget nextprops cljs-argv)]
(f C next-argv)))
:componentDidUpdate
(fn [C oldprops]
(let [p (aget oldprops cljs-props)
c (aget oldprops cljs-children)]
(f C p c)))
(let [old-argv (aget oldprops cljs-argv)]
(f C old-argv)))
:componentWillUnmount
(fn [C]
@ -202,11 +215,15 @@
(defn wrap-funs [fun-map]
(let [name (or (:displayName fun-map)
(when-let [r (:render fun-map)]
(when-let [r (or (:componentFunction fun-map)
(:render fun-map))]
(or (.-displayName r)
(.-name r))))
name1 (if (empty? name) (str (gensym "reagent")) name)]
(into {} (for [[k v] (assoc fun-map :displayName name1)]
name1 (if (empty? name) (str (gensym "reagent")) name)
fmap (if-let [cf (:componentFunction fun-map)]
(assoc fun-map :render cf)
fun-map)]
(into {} (for [[k v] (assoc fmap :displayName name1)]
[k (get-wrapper k v name1)]))))
(defn cljsify [body]

View File

@ -7,8 +7,7 @@
(def React reactimport/React)
(def cljs-props "cljsProps")
(def cljs-children "cljsChildren")
(def cljs-argv "cljsArgv")
(def cljs-level "cljsLevel")
(def isClient (not (nil? (try (.-document js/window)
@ -88,8 +87,12 @@
(def input-components #{(aget DOM "input")
(aget DOM "textarea")})
(defn extract-props [v]
(let [p (get v 1)]
(if (map? p) p)))
(defn get-props [this]
(-> this (aget "props") (aget cljs-props)))
(-> this (aget "props") (aget cljs-argv) extract-props))
(defn input-initial-state [this]
(let [props (get-props this)]
@ -106,7 +109,7 @@
(on-change e))))
(defn input-will-receive-props [this new-props]
(let [props (aget new-props cljs-props)]
(let [props (-> new-props (aget cljs-argv) extract-props)]
(.setState this #js {:value (:value props)
:checked (:checked props)})))
@ -119,12 +122,16 @@
(defn wrapped-render [this comp id-class]
(let [inprops (aget this "props")
props (aget inprops cljs-props)
argv (aget inprops cljs-argv)
level (aget inprops cljs-level)
props (get argv 1)
hasprops (or (nil? props) (map? props))
jsargs (->> (aget inprops cljs-children)
(map-into-array as-component (inc level)))
jsprops (convert-props props id-class)]
first-child (if hasprops 2 1)
jsargs (if (> (count argv) first-child)
(map-into-array as-component (inc level)
(subvec argv first-child))
(array))
jsprops (convert-props (if hasprops props) id-class)]
(when (input-components comp)
(input-render-setup this jsprops))
(.unshift jsargs jsprops)
@ -132,11 +139,9 @@
(defn wrapped-should-update [C nextprops nextstate]
(let [inprops (aget C "props")
p1 (aget inprops cljs-props)
c1 (aget inprops cljs-children)
p2 (aget nextprops cljs-props)
c2 (aget nextprops cljs-children)]
(not (util/equal-args p1 c1 p2 c2))))
a1 (aget inprops cljs-argv)
a2 (aget nextprops cljs-argv)]
(not (util/equal-args a1 a2))))
(defn wrap-component [comp extras name]
(let [def #js {:render
@ -175,7 +180,7 @@
(defn fn-to-class [f]
(let [spec (meta f)
withrender (merge spec {:render f})
withrender (assoc spec :component-function f)
res (reagent.core/create-class withrender)
wrapf (.-cljsReactClass res)]
(set! (.-cljsReactClass f) wrapf)
@ -195,15 +200,11 @@
(defn vec-to-comp [v level]
(assert (pos? (count v)))
(let [[tag props] v
hasmap (map? props)
first-child (if (or hasmap (nil? props)) 2 1)
c (as-class tag)
jsprops (js-obj cljs-props (if hasmap props)
cljs-children (if (> (count v) first-child)
(subvec v first-child))
cljs-level level)]
(when hasmap
(let [props (get v 1)
c (as-class (v 0))
jsprops (js-obj cljs-argv v
cljs-level level)]
(when (map? props)
(let [key (:key props)]
(when-not (nil? key)
(aset jsprops "key" key))))

View File

@ -35,18 +35,6 @@
(assert (map? p1))
(merge-style p1 (merge-class p1 (merge p1 p2))))))
(defn identical-parts [v1 v2]
;; Compare two vectors using identical?
(or (identical? v1 v2)
(let [end (count v1)]
(and (== end (count v2))
(loop [n 0]
(if (>= n end)
true
(if (identical? (nth v1 n) (nth v2 n))
(recur (inc n))
false)))))))
(def -not-found (js-obj))
(defn shallow-equal-maps [x y]
@ -70,7 +58,16 @@
(reduced false))))
true x))))
(defn equal-args [p1 c1 p2 c2]
[p1 c1 p2 c2]
(and (identical-parts c1 c2)
(shallow-equal-maps p1 p2)))
(defn equal-args [v1 v2]
;; Compare two vectors using identical?
(or (identical? v1 v2)
(let [end (count v1)]
(and (== end (count v2))
(loop [n 1]
(if (>= n end)
true
(if (or (identical? (v1 n) (v2 n))
(and (== 1 n)
(shallow-equal-maps (v1 1) (v2 1))))
(recur (inc n))
false)))))))

View File

@ -51,25 +51,28 @@
(when isClient
(let [ran (atom 0)
comp (reagent/create-class
{:component-did-mount #(swap! ran inc)
:render (fn [props children this]
(assert (map? props))
(swap! ran inc)
[:div (str "hi " (:foo props) ".")])})]
{:component-did-mount #(swap! ran inc)
:render
(fn [this]
(let [props (reagent/props this)]
(assert (map? props))
(assert (= props ((reagent/argv this) 1)))
(swap! ran inc)
[:div (str "hi " (:foo props) ".")]))})]
(with-mounted-component (comp {:foo "you"})
(fn [C div]
(swap! ran inc)
(is (found-in #"hi you" div))
(reagent/set-props C {:foo "there"})
(reagent/replace-args C [{:foo "there"}])
(is (found-in #"hi there" div))
(let [runs @ran]
(reagent/set-props C {:foo "there"})
(reagent/replace-args C [{:foo "there"}])
(is (found-in #"hi there" div))
(is (= runs @ran)))
(reagent/replace-props C {:foobar "not used"})
(reagent/replace-args C [{:foobar "not used"}])
(is (found-in #"hi ." div))))
(is (= 5 @ran)))))
@ -77,10 +80,12 @@
(when isClient
(let [ran (atom 0)
comp (reagent/create-class
{:get-initial-state (fn [])
:render (fn [props children this]
(swap! ran inc)
[:div (str "hi " (:foo (reagent/state this)))])})]
{:get-initial-state (fn [])
:render
(fn []
(let [this (reagent/current-component)]
(swap! ran inc)
[:div (str "hi " (:foo (reagent/state this)))]))})]
(with-mounted-component (comp)
(fn [C div]
(swap! ran inc)
@ -164,12 +169,13 @@
(deftest init-state-test
(when isClient
(let [ran (atom 0)
really-simple (fn [props children this]
(swap! ran inc)
(reagent/set-state this {:foo "foobar"})
(fn []
[:div (str "this is "
(:foo (reagent/state this)))]))]
really-simple (fn []
(let [this (reagent/current-component)]
(swap! ran inc)
(reagent/set-state this {:foo "foobar"})
(fn []
[:div (str "this is "
(:foo (reagent/state this)))])))]
(with-mounted-component [really-simple nil nil]
(fn [c div]
(swap! ran inc)