Merge branch 'delayed'

Conflicts:
	src/reagent/impl/template.cljs
This commit is contained in:
Dan Holmsand 2014-02-03 14:07:02 +01:00
commit aca69347b7
15 changed files with 618 additions and 99 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
index.html
assets/
news/
/news/
target
pom.xml
.lein-repl-history

View File

@ -26,10 +26,10 @@ runtest:
$(MAKE) run PROF=test,$(PROF)
runsite: setup
(sleep 3 && open "http://127.0.0.1:$(PORT)") &
(sleep 3 && open "http://127.0.0.1:$(PORT)/$$(basename $$PWD)") &
( trap "kill 0" SIGINT SIGTERM EXIT; \
( python -m SimpleHTTPServer $(PORT) & ); \
lein -o with-profile $(PROF) cljsbuild auto $(CLJSBUILD) )
( cd .. && python -m SimpleHTTPServer $(PORT) & ); \
lein -o with-profile $(PROF),prod cljsbuild auto $(CLJSBUILD) )
install: leinbuild
lein install

View File

@ -33,13 +33,14 @@
[github-badge]])
(defn ^:export mountdemo [p]
(when p (reset! page p))
(when p (page/set-start-page p))
(reagent/render-component [demo] (.-body js/document)))
(defn gen-page [p timestamp]
(reset! page p)
(let [body (reagent/render-component-to-string [demo])
title @page/title-atom]
title @page/title-atom
load-page (case p "index.html" "" p)]
(str "<!doctype html>
<html>
<head>
@ -53,7 +54,7 @@
<script type='text/javascript'
src='" (prefix "assets/demo.js") timestamp "'></script>
<script type='text/javascript'>
setTimeout(function() {demo.mountdemo('" p "')}, 200);
setTimeout(function() {demo.mountdemo('" load-page "')}, 200);
</script>
</body>
</html>")))

View File

@ -21,9 +21,7 @@
")
(defn src-for-names [srcmap names]
(string/join "\n" (-> srcmap
(select-keys names)
vals)))
(string/join "\n" (map srcmap names)))
(defn fun-map [src]
(-> src src-parts src-defs (assoc :ns nssrc)))
@ -36,10 +34,11 @@
(fn []
[:div
(when comp
[:div.demo-example
[:div.demo-example.clearfix
[:a.demo-example-hide {:on-click (fn [e]
(.preventDefault e)
(swap! showing not))}
(swap! showing not)
false)}
(if @showing "hide" "show")]
[:h3.demo-heading "Example "]
(when @showing
@ -47,6 +46,6 @@
[:div.simple-demo [comp]]
[comp]))])
(when @showing
[:div.demo-source
[:div.demo-source.clearfix
[:h3.demo-heading "Source"]
src])])))

View File

@ -4,6 +4,7 @@
[reagentdemo.syntax :refer-macros [get-source]]
[reagentdemo.page :refer [title link page-map]]
[reagentdemo.common :as common :refer [demo-component]]
[reagentdemo.news.async :as async]
[todomvc :as todomvc]))
(def funmap (-> "reagentdemo/news.cljs" get-source common/fun-map))
@ -44,7 +45,7 @@
(reset! undo-list nil)
(remove-watch state ::undo-watcher))}))
(defn undo-example []
(defn undo-example [{:keys [summary]}]
(let [head "Cloact becomes Reagent: Undo is trivial"]
[:div.reagent-demo
[:h1 [link {:href undo-example} head]]
@ -63,25 +64,32 @@
[:p "The API is otherwise unchanged, so a simple
search-and-replace should suffice."]
[:h2 "Undo the easy way"]
(if summary
[link {:href undo-example
:class 'news-read-more} "Read more"]
[:div.demo-text
[:p "To celebrate the undoing of the apparently disgusting name,
here is an example of how easy it is to add undo functionality
to Reagent components."]
[:h2 "Undo the easy way"]
[:p "It simply saves the old state whenever it changes, and
restores it when the button is clicked."]
[:p "To celebrate the undoing of the apparently disgusting
name, here is an example of how easy it is to add undo
functionality to Reagent components."]
[:p "The really nice thing about ClojureScript is that not only
is this easy and safe to do, courtesy of immutable data
structures, it is also efficient. ClojureScript figures out how
to represent ”changes” to maps and vectors efficiently, so that
you wont have to."]
[:p "It simply saves the old state whenever it changes, and
restores it when the button is clicked."]
[undo-demo-cleanup]]]))
[:p "The really nice thing about ClojureScript is that not
only is this easy and safe to do, courtesy of immutable data
structures, it is also efficient. ClojureScript figures out
how to represent ”changes” to maps and vectors efficiently,
so that you wont have to."]
[undo-demo-cleanup]])]]))
(defn main []
[undo-example])
[:div
[async/main {:summary true}]
[undo-example {:summary true}]])
(swap! page-map assoc
"news/cloact-reagent-undo-demo.html" undo-example)

View File

@ -0,0 +1,197 @@
(ns reagentdemo.news.async
(:require [reagent.core :as reagent :refer [atom]]
[reagent.debug :refer-macros [dbg println]]
[reagentdemo.syntax :refer-macros [get-source]]
[reagentdemo.page :refer [title link page-map]]
[reagentdemo.common :as common :refer [demo-component]]))
(def funmap (-> "reagentdemo/news/async.cljs" get-source common/fun-map))
(def src-for (partial common/src-for funmap))
(defn timing-wrapper [{f :component-fn}]
(let [start-time (atom nil)
render-time (atom nil)
now #(.now js/Date)
start #(reset! start-time (now))
stop #(reset! render-time (- (now) @start-time))
timed-f (with-meta f
{:component-will-mount start
:component-will-update start
:component-did-mount stop
:component-did-update stop})]
(fn [props children]
[:div
[:p [:em "render time: " @render-time "ms"]]
(into [timed-f props] children)])))
(def base-color (atom {:red 130 :green 160 :blue 120}))
(def ncolors (atom 20))
(def random-colors (atom nil))
(defn to-rgb [{:keys [red green blue]}]
(let [hex #(str (if (< % 16) "0")
(-> % js/Math.round (.toString 16)))]
(str "#" (hex red) (hex green) (hex blue))))
(defn tweak-color [{:keys [red green blue]}]
(let [rnd #(-> (js/Math.random) (* 256))
tweak #(-> % (+ (rnd)) (/ 2) js/Math.floor)]
{:red (tweak red) :green (tweak green) :blue (tweak blue)}))
(defn reset-random-colors []
(reset! random-colors
(repeatedly #(-> @base-color tweak-color to-rgb))))
(defn color-choose [{color-part :color-part}]
[:div.color-slider
(name color-part) " " (color-part @base-color)
[:input {:type "range" :min 0 :max 255
:value (color-part @base-color)
:on-change (fn [e]
(swap! base-color assoc
color-part (-> e .-target .-value int))
(reset-random-colors))}]])
(defn ncolors-choose []
[:div.color-slider
"number of color divs " @ncolors
[:input {:type "range" :min 0 :max 500
:value @ncolors
:on-change #(reset! ncolors (-> % .-target .-value))}]])
(defn color-plate [{color :color}]
[:div.color-plate
{:style {:background-color color}}])
(defn palette []
(let [color @base-color
n @ncolors]
[:div
[:div
[:p "base color: "]
[color-plate {:color (to-rgb color)}]]
[:div.color-samples
[:p n " random matching colors:"]
(map-indexed (fn [k v]
[color-plate {:key k :color v}])
(take n @random-colors))]]))
(defn color-demo []
(reset-random-colors)
(fn []
[:div
[:h2 "Matching colors"]
[color-choose {:color-part :red}]
[color-choose {:color-part :green}]
[color-choose {:color-part :blue}]
[ncolors-choose]
[timing-wrapper {:component-fn palette}]]))
(defn main [{:keys [summary]}]
(let [om-article {:href "http://swannodette.github.io/2013/12/17/the-future-of-javascript-mvcs/"}]
[:div.reagent-demo
[title "Reagent: Faster by waiting"]
[:h1 [link {:href main} "Faster by waiting"]]
[:div.demo-text
[:h2 "Reagent gets async rendering"]
[:p "Reagent already separates state from components. Now they
are also separated in time."]
[:p "From version 0.3.0, changes in application state (as
represented by Reagents " [:code "atom"] "s) are no longer
rendered immediately to the DOM. Instead, Reagent waits until
the browser is ready to repaint the window, and then all the
changes are rendered in one single go."]
(if summary
[link {:href main
:class 'news-read-more} "Read more"]
[:div.demo-text
[:p "This is good for all sorts of reasons:"]
[:ul
[:li "Reagent doesn't have to spend time doing renderings
that no one would ever see (because changes to application
state happened faster than the browser could repaint)."]
[:li "If two or more atoms are changed simultaneously, this
now leads to only one re-rendering, and not two."]
[:li "The new code does proper batching of renderings even
when changes to atoms are done outside of event
handlers (which is great for e.g core.async users)."]
[:li "Repaints can be synced by the browser with for example
CSS transitions, since Reagent uses requestAnimationFrame
to do the batching. That makes for example animations
smoother."]]
[:p "In short, Reagent renders less often, but at the right
times. For a much better description of why async rendering
is good, see David Nolens " [:a om-article "excellent
explanation here."]]
[:h2 "The bad news"]
[:p "Lunches in general tend to be non-free, and this is no
exception… The downside to async rendering is that you can no
longer depend on changes to atoms being immediately available
in the DOM. (Actually, you couldnt before either, since
React.js itself does batching inside event handlers.)"]
[:p "This may make testing a bit more verbose: you now have
to call " [:code "reagent.core/flush"] " to force Reagent to
synchronize state with the DOM."]
[:h2 "An example"]
[:p "Here is an example to (hopefully) demonstrate the
virtues of async rendering. It consists of a simple color
chooser (three sliders to set the red, green and blue
components of a base color), and shows the base color + a
bunch of divs in random matching colors. As soon as the base
color is changed, a new set of random colors is shown."]
[:p "If you change one of the base color components, the base
color should change immediately, and smoothly (on my Macbook
Air, rendering takes around 2ms, with 20 colored divs
showing)."]
[:p "But perhaps more interesting is to see what happens when
the updates cant be made smoothly (because the browser
simply cannot re-render the colored divs quickly enough). On
my machine, this starts to happen if I change the number of
divs shown to above 150 or so."]
[:p "As you increase the number of divs, youll notice that
the base color no longer changes quite so smoothly when you
move the color sliders."]
[:p "But the crucial point is that the sliders "
[:strong "still work"] ". Without async rendering, you could
quickly get into a situation where the browser hangs for a
while, doing updates corresponding to an old state. "]
[:p "With async rendering, the only thing that happens is
that the frame rate goes down."]
[:p "Btw, I find it quite impressive that React manages to
change 500 divs (12 full screens worth) in slightly more than
40ms. And even better: when I change the number of divs
shown, it only takes around 6ms to re-render the color
palette (because the individual divs dont have to be
re-rendered, divs are just added or removed from the DOM as
needed)."]
[demo-component
{:comp color-demo
:src (src-for
[:ns :timing-wrapper :base-color :ncolors
:random-colors :to-rgb :tweak-color
:reset-random-colors :color-choose :ncolors-choose
:palette :color-demo])}]])]]))
(swap! page-map assoc
"news/reagent-is-async.html" main)

View File

@ -8,27 +8,46 @@
[goog.history EventType]))
(def page (atom ""))
(def base-path (atom nil))
(def html5-history false)
(defn create-history []
(when reagent/is-client
(let [proto (-> js/window .-location .-protocol)]
(if (and (.isSupported Html5History)
(case proto "http:" true "https:" true false))
(doto (Html5History.)
(.setUseFragment false))
(do (set! html5-history true)
(doto (Html5History.)
(.setUseFragment false)))
(History.)))))
(defn setup-history []
(when-let [h (create-history)]
(events/listen h EventType/NAVIGATE
(fn [e] (reset! page (.-token e))))
(fn [e]
(reset! page (subs (.-token e)
(count @base-path)))
(reagent/flush)))
(add-watch page ::history (fn [_ _ oldp newp]
(.setToken h newp)))
(.setToken h (str @base-path newp))))
(.setEnabled h true)
h))
(def history (setup-history))
(defn set-start-page [p]
(when html5-history
;; Find base-path for html5 history
(let [loc (-> js/window .-location .-pathname)
split #".[^/]*"
loc-parts (re-seq split loc)
page-parts (re-seq split (case p "" "." p))
base (str (apply str
(drop-last (count page-parts) loc-parts))
"/")]
(reset! base-path (string/replace base #"^/" ""))))
(reset! page p))
(def title-atom (atom ""))
(def page-map (atom nil))
@ -55,14 +74,22 @@
:on-click (if history
(fn [e]
(.preventDefault e)
(reset! page href))
(reset! page href)
(set! (.-scrollTop (.-body js/document))
0))
identity))
children)))
(add-watch page ::title-watch
(fn [_ _ _ p]
;; First title on a page wins
(reset! title-atom "")))
(defn title [props children]
(let [name (first children)]
(if reagent/is-client
(let [title (aget (.getElementsByTagName js/document "title") 0)]
(set! (.-innerHTML title) name)))
(reset! title-atom name)
(when (= @title-atom "")
(if reagent/is-client
(let [title (aget (.getElementsByTagName js/document "title") 0)]
(set! (.-innerHTML title) name)))
(reset! title-atom name))
[:div]))

View File

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

View File

@ -5,6 +5,16 @@
box-sizing: border-box;
}
.clearfix:before, .clearfix:after {
content: " ";
display: table;
}
.clearfix:after {
clear: both;
}
div.nav {
position: absolute;
top: 0;
@ -65,6 +75,8 @@ ul.nav > li.brand > a {
font-family: 'HelveticaNeue-Light', 'Helvetica Neue', arial;
font-weight: normal;
line-height: 1.25em;
margin-top: 0.25em;
margin-bottom: 1em;
}
.reagent-demo > h1 > a {
@ -112,6 +124,10 @@ ul.nav > li.brand > a {
color: #444;
}
.demo-text > ul > li {
margin-bottom: 1em;
}
.demo-example {
background-color: #ebebeb;
}
@ -140,3 +156,25 @@ ul.nav > li.brand > a {
float: right;
cursor: pointer;
}
.news-read-more {
text-decoration: none;
font-size: 16px;
}
/* Color demo */
.color-plate {
float: left;
height: 100px;
width: 100px;
}
.color-slider > input {
width: 100%;
}
.color-samples {
clear: both;
padding-top: 0.5em;
}

View File

@ -1,6 +1,6 @@
(ns reagent.core
(:refer-clojure :exclude [partial atom])
(:refer-clojure :exclude [partial atom flush])
(:require-macros [reagent.debug :refer [dbg prn]])
(:require [reagent.impl.template :as tmpl]
[reagent.impl.component :as comp]
@ -13,8 +13,7 @@
(defn render-component
"Render a Reagent component into the DOM. The first argument may be either a
vector (using Reagent's Hiccup syntax), or a React component. The second argument
should be a DOM node.
vector (using Reagent's Hiccup syntax), or a React component. The second argument should be a DOM node.
Optionally takes a callback that is called when the component is in place.
@ -44,6 +43,9 @@ 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-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-unmount (fn [this])
:render (fn [props children this])}
@ -105,6 +107,15 @@ specially, like React's transferPropsTo."
[defaults props]
(util/merge-props defaults props))
(defn flush
"Render dirty components immediately to the DOM.
Note that this may not work in event handlers, since React.js does
batching of updates there."
[]
(comp/flush))
;; Ratom
@ -118,6 +129,11 @@ re-rendered."
;; Utilities
(defn next-tick
"Run f using requestAnimationFrame or equivalent."
[f]
(comp/next-tick f))
(defn partial
"Works just like clojure.core/partial, except that it is an IFn, and
the result can be compared with ="

View File

@ -1,7 +1,8 @@
(ns reagent.impl.component
(:refer-clojure :exclude [flush])
(:require [reagent.impl.template :as tmpl
:refer [cljs-props cljs-children React]]
:refer [cljs-props cljs-children cljs-level React]]
[reagent.impl.util :as util]
[reagent.ratom :as ratom]
[reagent.debug :refer-macros [dbg prn]]))
@ -35,7 +36,7 @@
(-> C js-props props-in-props))
(defn get-children [C]
(->> C js-props (aget cljs-children)))
(-> C js-props (aget cljs-children)))
(defn replace-props [C newprops]
(.setProps C (js-obj cljs-props newprops)))
@ -43,17 +44,67 @@
(defn set-props [C newprops]
(replace-props C (merge (get-props C) newprops)))
;;; Rendering
;;; Function wrapping
(defn fake-raf [f]
(js/setTimeout f 16))
(def next-tick
(if-not tmpl/isClient
fake-raf
(let [w js/window]
(or (.-requestAnimationFrame w)
(.-webkitRequestAnimationFrame w)
(.-mozRequestAnimationFrame w)
(.-msRequestAnimationFrame w)
fake-raf))))
(defn compare-levels [c1 c2]
(- (-> c1 js-props (aget cljs-level))
(-> c2 js-props (aget cljs-level))))
(defn run-queue [a]
;; sort components by level, to make sure parents
;; are rendered before children
(.sort a compare-levels)
(dotimes [i (alength a)]
(let [C (aget a i)]
(when (.-cljsIsDirty C)
(.forceUpdate C)))))
(deftype RenderQueue [^:mutable queue ^:mutable scheduled?]
Object
(queue-render [this C]
(.push queue C)
(.schedule this))
(schedule [this]
(when-not scheduled?
(set! scheduled? true)
(next-tick #(.run-queue this))))
(run-queue [_]
(let [q queue]
(set! queue (array))
(set! scheduled? false)
(run-queue q))))
(def render-queue (RenderQueue. (array) false))
(defn flush []
(.run-queue render-queue))
(defn queue-render [C]
(set! (.-cljsIsDirty C) true)
(.queue-render render-queue C))
(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)
(tmpl/as-component res (aget p cljs-level))
(if (fn? res)
(do-render C (set! (.-cljsRenderFn C) res))
res))]
@ -66,10 +117,13 @@
(ratom/make-reaction
#(do-render C (.-cljsRenderFn C))
:auto-run (if tmpl/isClient
#(.forceUpdate C)
identity))))
#(queue-render C)
identity))))
(ratom/run (.-cljsRatom C)))
;;; Function wrapping
(defn custom-wrapper [key f]
(case key
:getDefaultProps
@ -98,16 +152,22 @@
;; call f with oldprops newprops oldchildren newchildren
(f C p1 p2 c1 c2))))
:componentWillUpdate
(fn [C nextprops]
(let [p (aget nextprops cljs-props)
c (aget nextprops cljs-children)]
(f C p c)))
:componentDidUpdate
(fn [C oldprops]
(let [inprops (js-props C)
p (aget inprops cljs-props)
c (aget inprops cljs-children)]
(let [p (aget oldprops cljs-props)
c (aget oldprops cljs-children)]
(f C p c)))
:componentWillUnmount
(fn [C]
(ratom/dispose! (.-cljsRatom C))
(set! (.-cljsIsDirty C) false)
(when f (f C)))
:render

View File

@ -3,12 +3,13 @@
(:require [clojure.string :as string]
[reagent.impl.reactimport :as reactimport]
[reagent.impl.util :as util]
[reagent.debug :refer-macros [dbg prn println]]))
[reagent.debug :refer-macros [dbg prn println log]]))
(def React reactimport/React)
(def cljs-props "cljsProps")
(def cljs-children "cljsChildren")
(def cljs-level "cljsLevel")
(def isClient (not (nil? (try (.-document js/window)
(catch js/Object e nil)))))
@ -35,13 +36,22 @@
(def cached-prop-name (memoize undash-prop-name))
(def cached-style-name (memoize dash-to-camel))
(defn to-js-val [v]
(if-not (ifn? v)
v
(cond (keyword? v) (name v)
(symbol? v) (str v)
(coll? v) (clj->js v)
:else (fn [& args] (apply v args)))))
(defn convert-prop-value [val]
(cond (map? val) (let [obj (js-obj)]
(doseq [[k v] val]
(aset obj (cached-style-name k) (clj->js v)))
obj)
(ifn? val) (fn [& args] (apply val args))
:else (clj->js val)))
(if (map? val)
(reduce-kv (fn [res k v]
(doto res
(aset (cached-prop-name k)
(to-js-val v))))
(js-obj) val)
(to-js-val val)))
(defn set-id-class [props [id class]]
(aset props "id" (or (aget props "id") id))
@ -57,28 +67,67 @@
(identical? (type props) js/Object) props
:else (let [objprops (js-obj)]
(when-not is-empty
(doseq [[k v] props]
(aset objprops (cached-prop-name k)
(convert-prop-value v))))
(reduce-kv (fn [o k v]
(doto o (aset (cached-prop-name k)
(convert-prop-value v))))
objprops props))
(when-not (nil? id-class)
(set-id-class objprops id-class))
objprops))))
(defn map-into-array [f coll]
(let [a (into-array coll)]
(dotimes [i (alength a)]
(aset a i (f (aget a i))))
a))
(defn map-into-array [f arg coll]
(reduce (fn [a x]
(doto a
(.push (f x arg))))
#js [] coll))
(declare as-component)
(def DOM (aget React "DOM"))
(def input-components #{(aget DOM "input")
(aget DOM "textarea")})
(defn get-props [this]
(-> this (aget "props") (aget cljs-props)))
(defn input-initial-state [this]
(let [props (get-props this)]
#js {:value (:value props)
:checked (:checked props)}))
(defn input-handle-change [this e]
(let [props (get-props this)
on-change (or (props :on-change) (props "onChange"))]
(when-not (nil? on-change)
(let [target (.-target e)]
(.setState this #js {:value (.-value target)
:checked (.-checked target)}))
(on-change e))))
(defn input-will-receive-props [this new-props]
(let [props (aget new-props cljs-props)]
(.setState this #js {:value (:value props)
:checked (:checked props)})))
(defn input-render-setup [this jsprops]
(let [state (aget this "state")]
(doto jsprops
(aset "value" (.-value state))
(aset "checked" (.-checked state))
(aset "onChange" (aget this "handleChange")))))
(defn wrapped-render [this comp id-class]
(let [inprops (aget this "props")
props (aget inprops cljs-props)
level (aget inprops cljs-level)
hasprops (or (nil? props) (map? props))
jsargs (->> (aget inprops cljs-children)
(map-into-array as-component))]
(.unshift jsargs (convert-props props id-class))
(map-into-array as-component (inc level)))
jsprops (convert-props props id-class)]
(when (input-components comp)
(input-render-setup this jsprops))
(.unshift jsargs jsprops)
(.apply comp nil jsargs)))
(defn wrapped-should-update [C nextprops nextstate]
@ -90,20 +139,25 @@
(not (util/equal-args p1 c1 p2 c2))))
(defn wrap-component [comp extras name]
(.createClass React (js-obj "render"
#(this-as C (wrapped-render C comp extras))
"shouldComponentUpdate"
#(this-as C (wrapped-should-update C %1 %2))
"displayName"
(or name "ComponentWrapper"))))
(let [def #js {:render
#(this-as C (wrapped-render C comp extras))
:shouldComponentUpdate
#(this-as C (wrapped-should-update C %1 %2))
:displayName (or name "ComponentWrapper")}]
(when (input-components comp)
(doto def
(aset "shouldComponentUpdate" nil)
(aset "getInitialState" #(this-as C (input-initial-state C)))
(aset "handleChange" #(this-as C (input-handle-change C %)))
(aset "componentWillReceiveProps"
#(this-as C (input-will-receive-props C %)))))
(.createClass React def)))
;; From Weavejester's Hiccup, via pump:
(def ^{:doc "Regular expression that parses a CSS-style id and class
from a tag name."}
re-tag #"([^\s\.#]+)(?:#([^\s\.#]+))?(?:\.([^\s#]+))?")
(def DOM (aget React "DOM"))
(defn parse-tag [hiccup-tag]
(let [[tag id class] (->> hiccup-tag name (re-matches re-tag) next)
comp (aget DOM tag)
@ -139,7 +193,7 @@
(set! (.-cljsReactClass tag) (wrap-component tag nil nil))
(fn-to-class tag)))))))
(defn vec-to-comp [v]
(defn vec-to-comp [v level]
(assert (pos? (count v)))
(let [[tag props] v
hasmap (map? props)
@ -147,14 +201,17 @@
c (as-class tag)
jsprops (js-obj cljs-props (if hasmap props)
cljs-children (if (> (count v) first-child)
(subvec v first-child)))]
(subvec v first-child))
cljs-level level)]
(when hasmap
(let [key (:key props)]
(when-not (nil? key)
(aset jsprops "key" key))))
(c jsprops)))
(defn as-component [x]
(cond (vector? x) (vec-to-comp x)
(seq? x) (map-into-array as-component x)
true x))
(defn as-component
([x] (as-component x 0))
([x level]
(cond (vector? x) (vec-to-comp x level)
(seq? x) (map-into-array as-component level x)
true x)))

View File

@ -52,15 +52,19 @@
(defn shallow-equal-maps [x y]
;; Compare two maps, using keyword-identical? on all values
(or (identical? x y)
(and (== (count x) (count y))
(and (map? x)
(map? y)
(== (count x) (count y))
(reduce-kv (fn [res k v]
(let [yv (get y k -not-found)]
(if (or (keyword-identical? v yv)
;; hack to allow reagent.core/partial and :style
;; maps to be compared with =
(and (or
(keyword-identical? k :style)
(identical? (type v) partial-ifn))
;; Allow :style maps, symbols
;; and reagent/partial
;; to be compared properly
(and (keyword-identical? k :style)
(shallow-equal-maps v yv))
(and (or (identical? (type v) partial-ifn)
(symbol? v))
(= v yv)))
res
(reduced false))))

View File

@ -9,11 +9,12 @@
(defn running [] @-running)
(defn- capture-derefed [f]
;; TODO: Get rid of allocation.
(binding [*ratom-context* (clojure.core/atom #{})]
[(f) @*ratom-context*]))
(defn- notify-deref-watcher! [derefable]
(when-not (or (nil? *ratom-context*))
(when-not (nil? *ratom-context*)
(swap! *ratom-context* conj derefable)))
(deftype RAtom [state meta validator watches]
@ -36,8 +37,10 @@
IWatchable
(-notify-watches [this oldval newval]
(doseq [[key f] watches]
(f key this oldval newval)))
(reduce-kv (fn [_ key f]
(f key this oldval newval)
nil)
nil watches))
(-add-watch [this key f]
(set! (.-watches this) (assoc watches key f)))
(-remove-watch [this key]
@ -63,8 +66,10 @@
(-handle-change [k sender oldval newval]))
(defn- call-watches [obs watches oldval newval]
(doseq [[k wf] watches]
(wf k obs oldval newval)))
(reduce-kv (fn [_ key f]
(f key obs oldval newval)
nil)
nil watches))
(deftype Reaction [f ^:mutable state ^:mutable dirty? ^:mutable active?
^:mutable watching ^:mutable watches

View File

@ -12,6 +12,8 @@
(def isClient (not (nil? (try (.-document js/window)
(catch js/Object e nil)))))
(def rflush reagent/flush)
(defn add-test-div [name]
(let [doc js/document
body (.-body js/document)
@ -23,7 +25,8 @@
(when isClient
(let [div (add-test-div "_testreagent")]
(let [comp (reagent/render-component comp div #(f comp div))]
(reagent/unmount-component-at-node div)))))
(reagent/unmount-component-at-node div)
(reagent/flush)))))
(defn found-in [re div]
(let [res (.-innerHTML div)]
@ -95,27 +98,68 @@
(let [ran (atom 0)
runs (running)
val (atom 0)
secval (atom 0)
v1 (reaction @val)
comp (fn []
(swap! ran inc)
[:div (str "val " @v1)])]
[:div (str "val " @v1 @val @secval)])]
(with-mounted-component [comp]
(fn [C div]
(swap! ran inc)
(reagent/flush)
(is (not= runs (running)))
(is (found-in #"val 0" div))
(is (= 2 @ran))
(is (= 1 @ran))
(reset! secval 1)
(reset! secval 0)
(reset! val 1)
(reset! val 2)
(reset! val 1)
(reagent/flush)
(is (found-in #"val 1" div))
(is (= 3 @ran))
(is (= 2 @ran))
;; should not be rendered
(reset! val 1)
(reagent/flush)
(is (found-in #"val 1" div))
(is (= 3 @ran))))
(is (= 2 @ran))))
(is (= runs (running)))
(is (= 3 @ran)))))
(is (= 2 @ran)))))
(deftest batched-update-test []
(when isClient
(let [ran (atom 0)
v1 (atom 0)
v2 (atom 0)
c2 (fn [{val :val}]
(swap! ran inc)
(assert (= @v1 val))
[:div @v2])
c1 (fn []
(swap! ran inc)
[:div @v1
[c2 {:val @v1}]])]
(with-mounted-component [c1]
(fn [c div]
(rflush)
(is (= @ran 2))
(swap! v2 inc)
(is (= @ran 2))
(rflush)
(is (= @ran 3))
(swap! v1 inc)
(rflush)
(is (= @ran 5))
(swap! v2 inc)
(swap! v1 inc)
(rflush)
(is (= @ran 7))
(swap! v1 inc)
(swap! v1 inc)
(swap! v2 inc)
(rflush)
(is (= @ran 9)))))))
(deftest init-state-test
(when isClient
@ -132,6 +176,69 @@
(is (found-in #"this is foobar" div))))
(is (= 2 @ran)))))
(deftest shoud-update-test
(when isClient
(let [parent-ran (atom 0)
child-ran (atom 0)
child-props (atom nil)
f (fn [])
f1 (fn [])
child (fn [p]
(swap! child-ran inc)
[:div (:val p)])
parent(fn []
(swap! parent-ran inc)
[:div "child-foo" [child @child-props]])]
(with-mounted-component [parent nil nil]
(fn [c div]
(rflush)
(is (= @child-ran 1))
(is (found-in #"child-foo" div))
(do (reset! child-props {:style {:display :none}})
(rflush))
(is (= @child-ran 2))
(do (reset! child-props {:style {:display :none}})
(rflush))
(is (= @child-ran 2) "keyw is equal")
(do (reset! child-props {:class :foo}) (rflush))
(is (= @child-ran 3))
(do (reset! child-props {:class :foo}) (rflush))
(is (= @child-ran 3))
(do (reset! child-props {:class 'foo}) (rflush))
(is (= @child-ran 4) "symbols are different from keyw")
(do (reset! child-props {:class 'foo}) (rflush))
(is (= @child-ran 4) "symbols are equal")
(do (reset! child-props {:style {:color 'red}}) (rflush))
(is (= @child-ran 5))
(do (reset! child-props {:on-change (reagent/partial f)})
(rflush))
(is (= @child-ran 6))
(do (reset! child-props {:on-change (reagent/partial f)})
(rflush))
(is (= @child-ran 6))
(do (reset! child-props {:on-change (reagent/partial f1)})
(rflush))
(is (= @child-ran 7)))))))
(deftest dirty-test
(when isClient
(let [ran (atom 0)
state (atom 0)
really-simple (fn [props children this]
(swap! ran inc)
(if (= @state 1)
(reset! state 3))
[:div (str "state=" @state)])]
(with-mounted-component [really-simple nil nil]
(fn [c div]
(is (= 1 @ran))
(is (found-in #"state=0" div))
(reset! state 1)
(rflush)
(is (= 2 @ran))
(is (found-in #"state=3" div))))
(is (= 2 @ran)))))
(defn as-string [comp]
(reagent/render-component-to-string comp))