mirror of https://github.com/status-im/reagent.git
Merge branch 'delayed'
Conflicts: src/reagent/impl/template.cljs
This commit is contained in:
commit
aca69347b7
|
@ -1,6 +1,6 @@
|
|||
index.html
|
||||
assets/
|
||||
news/
|
||||
/news/
|
||||
target
|
||||
pom.xml
|
||||
.lein-repl-history
|
||||
|
|
6
Makefile
6
Makefile
|
@ -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
|
||||
|
|
|
@ -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>")))
|
||||
|
|
|
@ -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])])))
|
||||
|
|
|
@ -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 won’t 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 won’t 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)
|
||||
|
|
|
@ -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 Reagent’s " [: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 Nolen’s " [: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 couldn’t 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 can’t 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, you’ll 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 don’t 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)
|
|
@ -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]))
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 ="
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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))))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
Loading…
Reference in New Issue