From eb381f13bb89d8eb7c1be3d84c793b3e56e99452 Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Mon, 27 Jan 2014 13:37:59 +0100 Subject: [PATCH 01/19] Start experimenting with batched rendering with requestAnimationFrame --- demo/reagentdemo/common.cljs | 3 ++- project.clj | 2 +- src/reagent/core.cljs | 6 ++++- src/reagent/impl/component.cljs | 47 ++++++++++++++++++++++++++++++--- test/testcloact.cljs | 19 ++++++++----- 5 files changed, 65 insertions(+), 12 deletions(-) diff --git a/demo/reagentdemo/common.cljs b/demo/reagentdemo/common.cljs index b3917d2..7d12cd1 100644 --- a/demo/reagentdemo/common.cljs +++ b/demo/reagentdemo/common.cljs @@ -39,7 +39,8 @@ [:div.demo-example [: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 diff --git a/project.clj b/project.clj index 6a9c215..95b9a3b 100644 --- a/project.clj +++ b/project.clj @@ -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" diff --git a/src/reagent/core.cljs b/src/reagent/core.cljs index f5c70b1..9306331 100644 --- a/src/reagent/core.cljs +++ b/src/reagent/core.cljs @@ -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] @@ -105,6 +105,10 @@ specially, like React's transferPropsTo." [defaults props] (util/merge-props defaults props)) +(defn flush [] + (comp/flush)) + + ;; Ratom diff --git a/src/reagent/impl/component.cljs b/src/reagent/impl/component.cljs index d9964d3..eaaf0d1 100644 --- a/src/reagent/impl/component.cljs +++ b/src/reagent/impl/component.cljs @@ -1,5 +1,6 @@ (ns reagent.impl.component + (:refer-clojure :exclude [flush]) (:require [reagent.impl.template :as tmpl :refer [cljs-props cljs-children React]] [reagent.impl.util :as util] @@ -43,10 +44,46 @@ (defn set-props [C newprops] (replace-props C (merge (get-props C) newprops))) +;;; Rendering -;;; Function wrapping +(defn next-tick [f] + (if (.-requestAnimationFrame js/window) + (js/requestAnimationFrame f) + (js/setTimeout f 16))) + +(defn run-queue [v] + (doseq [C v] + (when-not (.-cljsIsDirty C) + (dbg C)) + (when (.-cljsIsDirty C) + (.forceUpdate C)))) + +(deftype RenderQueue [^:mutable queue ^:mutable scheduled?] + Object + (queue-render [this C] + (set! queue (conj queue C)) + (.schedule this)) + (schedule [this] + (when-not scheduled? + (set! scheduled? true) + (next-tick #(.run-queue this)))) + (run-queue [_] + (let [q queue] + (set! queue (empty queue)) + (set! scheduled? false) + (run-queue q)))) + +(def render-queue (RenderQueue. [] 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) @@ -66,10 +103,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 @@ -108,6 +148,7 @@ :componentWillUnmount (fn [C] (ratom/dispose! (.-cljsRatom C)) + (set! (.-cljsIsDirty C) false) (when f (f C))) :render diff --git a/test/testcloact.cljs b/test/testcloact.cljs index 5b3e65d..2235f55 100644 --- a/test/testcloact.cljs +++ b/test/testcloact.cljs @@ -95,27 +95,34 @@ (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 init-state-test (when isClient From dfe0eb41d5792ae884ed80f5e6bfd5ac58b43dfd Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Mon, 27 Jan 2014 16:17:37 +0100 Subject: [PATCH 02/19] Batch updates properly by rendering parents before children --- src/reagent/core.cljs | 1 + src/reagent/impl/component.cljs | 28 +++++++++++++++---------- src/reagent/impl/template.cljs | 23 ++++++++++++--------- test/testcloact.cljs | 36 +++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 20 deletions(-) diff --git a/src/reagent/core.cljs b/src/reagent/core.cljs index 9306331..68d4c8b 100644 --- a/src/reagent/core.cljs +++ b/src/reagent/core.cljs @@ -44,6 +44,7 @@ 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-did-mount (fn [this]) :component-did-update (fn [this old-props old-children]) :component-will-unmount (fn [this]) :render (fn [props children this])} diff --git a/src/reagent/impl/component.cljs b/src/reagent/impl/component.cljs index eaaf0d1..f134f08 100644 --- a/src/reagent/impl/component.cljs +++ b/src/reagent/impl/component.cljs @@ -2,7 +2,7 @@ (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]])) @@ -51,17 +51,23 @@ (js/requestAnimationFrame f) (js/setTimeout f 16))) -(defn run-queue [v] - (doseq [C v] - (when-not (.-cljsIsDirty C) - (dbg C)) - (when (.-cljsIsDirty C) - (.forceUpdate C)))) +(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] - (set! queue (conj queue C)) + (.push queue C) (.schedule this)) (schedule [this] (when-not scheduled? @@ -69,11 +75,11 @@ (next-tick #(.run-queue this)))) (run-queue [_] (let [q queue] - (set! queue (empty queue)) + (set! queue (array)) (set! scheduled? false) (run-queue q)))) -(def render-queue (RenderQueue. [] false)) +(def render-queue (RenderQueue. (array) false)) (defn flush [] (.run-queue render-queue)) @@ -90,7 +96,7 @@ ;; 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))] diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs index 8a7eb35..a19ee2d 100644 --- a/src/reagent/impl/template.cljs +++ b/src/reagent/impl/template.cljs @@ -9,6 +9,7 @@ (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))))) @@ -64,10 +65,10 @@ (set-id-class objprops id-class)) objprops)))) -(defn map-into-array [f coll] +(defn map-into-array [f arg coll] (let [a (into-array coll)] (dotimes [i (alength a)] - (aset a i (f (aget a i)))) + (aset a i (f (aget a i) arg))) a)) (declare as-component) @@ -75,9 +76,10 @@ (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))] + (map-into-array as-component (inc level)))] (.unshift jsargs (convert-props props id-class)) (.apply comp nil jsargs))) @@ -139,7 +141,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 +149,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))) diff --git a/test/testcloact.cljs b/test/testcloact.cljs index 2235f55..65d54f9 100644 --- a/test/testcloact.cljs +++ b/test/testcloact.cljs @@ -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) @@ -124,6 +126,40 @@ (is (= runs (running))) (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 (let [ran (atom 0) From 4a537af429a72964d34afeb20a0604a461b6c9f7 Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Mon, 27 Jan 2014 16:26:21 +0100 Subject: [PATCH 03/19] Use prefixed requestAnimationFrames if available --- src/reagent/impl/component.cljs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/reagent/impl/component.cljs b/src/reagent/impl/component.cljs index f134f08..10975fc 100644 --- a/src/reagent/impl/component.cljs +++ b/src/reagent/impl/component.cljs @@ -46,10 +46,18 @@ ;;; Rendering -(defn next-tick [f] - (if (.-requestAnimationFrame js/window) - (js/requestAnimationFrame f) - (js/setTimeout f 16))) +(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)) From f1bbd7d99eeb98435ef41454d99a3d6451a677ee Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Mon, 27 Jan 2014 22:30:42 +0100 Subject: [PATCH 04/19] Handle html5 history better --- Makefile | 4 ++-- demo/demo.cljs | 7 ++++--- demo/reagentdemo/page.cljs | 25 +++++++++++++++++++++---- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index ec88f9d..393460b 100644 --- a/Makefile +++ b/Makefile @@ -26,9 +26,9 @@ 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) & ); \ + ( cd .. && python -m SimpleHTTPServer $(PORT) & ); \ lein -o with-profile $(PROF) cljsbuild auto $(CLJSBUILD) ) install: leinbuild diff --git a/demo/demo.cljs b/demo/demo.cljs index 32bb37a..7d1fb95 100644 --- a/demo/demo.cljs +++ b/demo/demo.cljs @@ -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 " @@ -53,7 +54,7 @@ "))) diff --git a/demo/reagentdemo/page.cljs b/demo/reagentdemo/page.cljs index adaba73..4c32eb9 100644 --- a/demo/reagentdemo/page.cljs +++ b/demo/reagentdemo/page.cljs @@ -8,27 +8,44 @@ [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))))) (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)) From 8b4ec9fda61eb20de55154c9bc3f14b193581d2d Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Tue, 28 Jan 2014 17:23:39 +0100 Subject: [PATCH 05/19] Make managed inputs work better with async rendering --- src/reagent/impl/template.cljs | 55 +++++++++++++++++++++++++++------- test/testcloact.cljs | 3 +- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs index a19ee2d..522e7cc 100644 --- a/src/reagent/impl/template.cljs +++ b/src/reagent/impl/template.cljs @@ -3,7 +3,7 @@ (: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) @@ -73,14 +73,43 @@ (declare as-component) +(def DOM (aget React "DOM")) + +(def input-components #{(aget DOM "input")}) + +(defn input-initial-state [] + (this-as this + (let [props (-> this (aget "props") (aget cljs-props))] + #js {:value (:value props)}))) + +(defn input-handle-change [e] + (this-as this + (let [props (-> this (aget "props") (aget cljs-props)) + on-change (or (props :on-change) (props "onChange"))] + (when-not (nil? on-change) + (.setState this #js {:value (-> e .-target .-value)}) + (on-change e))))) + +(defn input-will-receive-props [new-props] + (this-as this + (let [props (aget new-props cljs-props)] + (.setState this #js {:value (:value props)})))) + +(defn input-render-setup [this jsprops] + (aset jsprops "value" (-> this (aget "state") (aget "value"))) + (aset jsprops "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 (inc level)))] - (.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] @@ -92,20 +121,24 @@ (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-obj "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 "getInitialState" input-initial-state) + (aset "handleChange" input-handle-change) + (aset "componentWillReceiveProps" input-will-receive-props))) + (.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 [tag] (let [[tag id class] (->> tag name (re-matches re-tag) next) comp (aget DOM tag) diff --git a/test/testcloact.cljs b/test/testcloact.cljs index 65d54f9..26f827f 100644 --- a/test/testcloact.cljs +++ b/test/testcloact.cljs @@ -25,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)] From f654c6663f1e5a91079aaef627918e0506c3e11b Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Tue, 28 Jan 2014 19:34:06 +0100 Subject: [PATCH 06/19] Make textarea, radio buttons and checkboxes async-friendly And clean up code a little --- src/reagent/impl/template.cljs | 41 +++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs index 522e7cc..3dd2b14 100644 --- a/src/reagent/impl/template.cljs +++ b/src/reagent/impl/template.cljs @@ -75,29 +75,40 @@ (def DOM (aget React "DOM")) -(def input-components #{(aget DOM "input")}) +(def input-components #{(aget DOM "input") + (aget DOM "textarea")}) + +(defn get-props [this] + (-> this (aget "props") (aget cljs-props))) (defn input-initial-state [] (this-as this - (let [props (-> this (aget "props") (aget cljs-props))] - #js {:value (:value props)}))) + (let [props (get-props this)] + #js {:value (:value props) + :checked (:checked props)}))) (defn input-handle-change [e] (this-as this - (let [props (-> this (aget "props") (aget cljs-props)) + (let [props (get-props this) on-change (or (props :on-change) (props "onChange"))] (when-not (nil? on-change) - (.setState this #js {:value (-> e .-target .-value)}) - (on-change e))))) + (on-change e) + (let [target (.-target e)] + (.setState this #js {:value (.-value target) + :checked (.-checked target)})))))) (defn input-will-receive-props [new-props] (this-as this (let [props (aget new-props cljs-props)] - (.setState this #js {:value (:value props)})))) + (.setState this #js {:value (:value props) + :checked (:checked props)})))) (defn input-render-setup [this jsprops] - (aset jsprops "value" (-> this (aget "state") (aget "value"))) - (aset jsprops "onChange" (aget this "handleChange"))) + (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") @@ -121,14 +132,14 @@ (not (util/equal-args p1 c1 p2 c2)))) (defn wrap-component [comp extras name] - (let [def (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" input-initial-state) (aset "handleChange" input-handle-change) (aset "componentWillReceiveProps" input-will-receive-props))) From e055fbad266d05df2bdf83122eb516d8f28b5d0e Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Tue, 28 Jan 2014 20:18:19 +0100 Subject: [PATCH 07/19] Make Closure stop whining about "dangerous use of this in static method" --- src/reagent/impl/template.cljs | 42 ++++++++++++++++------------------ 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs index 3dd2b14..3d0d264 100644 --- a/src/reagent/impl/template.cljs +++ b/src/reagent/impl/template.cljs @@ -81,27 +81,24 @@ (defn get-props [this] (-> this (aget "props") (aget cljs-props))) -(defn input-initial-state [] - (this-as this - (let [props (get-props this)] - #js {:value (:value props) - :checked (:checked props)}))) +(defn input-initial-state [this] + (let [props (get-props this)] + #js {:value (:value props) + :checked (:checked props)})) -(defn input-handle-change [e] - (this-as this - (let [props (get-props this) - on-change (or (props :on-change) (props "onChange"))] - (when-not (nil? on-change) - (on-change e) - (let [target (.-target e)] - (.setState this #js {:value (.-value target) - :checked (.-checked target)})))))) +(defn input-handle-change [this e] + (let [props (get-props this) + on-change (or (props :on-change) (props "onChange"))] + (when-not (nil? on-change) + (on-change e) + (let [target (.-target e)] + (.setState this #js {:value (.-value target) + :checked (.-checked target)}))))) -(defn input-will-receive-props [new-props] - (this-as this - (let [props (aget new-props cljs-props)] - (.setState this #js {:value (:value props) - :checked (:checked props)})))) +(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")] @@ -140,9 +137,10 @@ (when (input-components comp) (doto def (aset "shouldComponentUpdate" nil) - (aset "getInitialState" input-initial-state) - (aset "handleChange" input-handle-change) - (aset "componentWillReceiveProps" input-will-receive-props))) + (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: From a91214e434ffb447c68017c6cfc91b961401085d Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Tue, 28 Jan 2014 20:19:10 +0100 Subject: [PATCH 08/19] Add next-tick as utility on core.cljs --- src/reagent/core.cljs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/reagent/core.cljs b/src/reagent/core.cljs index 68d4c8b..9a99e92 100644 --- a/src/reagent/core.cljs +++ b/src/reagent/core.cljs @@ -123,6 +123,9 @@ re-rendered." ;; Utilities +(defn next-tick [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 =" From b45a215f52738cc4fbc88602dab90f2dd9ba7520 Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Wed, 29 Jan 2014 11:00:05 +0100 Subject: [PATCH 09/19] Speed up conversion to js a lot by using reduce-kv --- src/reagent/impl/template.cljs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs index 3d0d264..8546a45 100644 --- a/src/reagent/impl/template.cljs +++ b/src/reagent/impl/template.cljs @@ -36,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" id) @@ -58,9 +67,10 @@ (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)))) @@ -90,10 +100,10 @@ (let [props (get-props this) on-change (or (props :on-change) (props "onChange"))] (when-not (nil? on-change) - (on-change e) (let [target (.-target e)] (.setState this #js {:value (.-value target) - :checked (.-checked target)}))))) + :checked (.-checked target)})) + (on-change e)))) (defn input-will-receive-props [this new-props] (let [props (aget new-props cljs-props)] From 344e9e5db57a15cefbb0277d702ab479686cf629 Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Wed, 29 Jan 2014 11:40:07 +0100 Subject: [PATCH 10/19] Make should-component-update a little faster for :style attrs --- src/reagent/impl/util.cljs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/reagent/impl/util.cljs b/src/reagent/impl/util.cljs index cd81cd1..e3e3a28 100644 --- a/src/reagent/impl/util.cljs +++ b/src/reagent/impl/util.cljs @@ -56,12 +56,12 @@ (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)) - (= v yv))) + ;; Allow :style maps and reagent/partial + ;; and :style maps to be compared properly + (and (keyword-identical? k :style) + (shallow-equal-maps v yv)) + (and (identical? (type v) partial-ifn) + (= y yv))) res (reduced false)))) true x)))) From 702f0c323817900b9e4c290cb9d182cdbcd4a435 Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Wed, 29 Jan 2014 11:53:45 +0100 Subject: [PATCH 11/19] Make testing demo a little easier --- Makefile | 2 +- demo/reagentdemo/page.cljs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 393460b..f0c9969 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ runsite: setup (sleep 3 && open "http://127.0.0.1:$(PORT)/$$(basename $$PWD)") & ( trap "kill 0" SIGINT SIGTERM EXIT; \ ( cd .. && python -m SimpleHTTPServer $(PORT) & ); \ - lein -o with-profile $(PROF) cljsbuild auto $(CLJSBUILD) ) + lein -o with-profile $(PROF),prod cljsbuild auto $(CLJSBUILD) ) install: leinbuild lein install diff --git a/demo/reagentdemo/page.cljs b/demo/reagentdemo/page.cljs index 4c32eb9..eb1c244 100644 --- a/demo/reagentdemo/page.cljs +++ b/demo/reagentdemo/page.cljs @@ -24,8 +24,10 @@ (defn setup-history [] (when-let [h (create-history)] (events/listen h EventType/NAVIGATE - (fn [e] (reset! page (subs (.-token e) - (count @base-path))))) + (fn [e] + (reset! page (subs (.-token e) + (count @base-path))) + (reagent/flush))) (add-watch page ::history (fn [_ _ oldp newp] (.setToken h (str @base-path newp)))) (.setEnabled h true) From d3407ff664a089769e0ff219f6dad0fc48826b3a Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Thu, 30 Jan 2014 10:34:41 +0100 Subject: [PATCH 12/19] Consider symbols to be "identical" And test some more obscure should-component-update variants. --- src/reagent/impl/util.cljs | 14 +++++++----- test/testcloact.cljs | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/reagent/impl/util.cljs b/src/reagent/impl/util.cljs index e3e3a28..8e582b6 100644 --- a/src/reagent/impl/util.cljs +++ b/src/reagent/impl/util.cljs @@ -52,16 +52,20 @@ (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) - ;; Allow :style maps and reagent/partial - ;; and :style maps to be compared properly + ;; Allow :style maps, symbols + ;; and reagent/partial + ;; to be compared properly (and (keyword-identical? k :style) (shallow-equal-maps v yv)) - (and (identical? (type v) partial-ifn) - (= y yv))) + (and (or (identical? (type v) partial-ifn) + (symbol? v)) + (= v yv))) res (reduced false)))) true x)))) diff --git a/test/testcloact.cljs b/test/testcloact.cljs index 26f827f..5e92df6 100644 --- a/test/testcloact.cljs +++ b/test/testcloact.cljs @@ -176,6 +176,51 @@ (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))))))) + + (defn as-string [comp] (reagent/render-component-to-string comp)) From 35bc3367432796d6a91eaf25f89235003b778cce Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Thu, 30 Jan 2014 22:29:42 +0100 Subject: [PATCH 13/19] Add a little demo --- .gitignore | 2 +- demo/reagentdemo/common.cljs | 8 +-- demo/reagentdemo/news.cljs | 5 +- demo/reagentdemo/news/async.cljs | 106 +++++++++++++++++++++++++++++++ demo/reagentdemo/page.cljs | 14 ++-- site/demo.css | 23 +++++++ 6 files changed, 147 insertions(+), 11 deletions(-) create mode 100644 demo/reagentdemo/news/async.cljs diff --git a/.gitignore b/.gitignore index 04c5219..5387438 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ index.html assets/ -news/ +/news/ target pom.xml .lein-repl-history diff --git a/demo/reagentdemo/common.cljs b/demo/reagentdemo/common.cljs index 7d12cd1..14d2ef3 100644 --- a/demo/reagentdemo/common.cljs +++ b/demo/reagentdemo/common.cljs @@ -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,7 +34,7 @@ (fn [] [:div (when comp - [:div.demo-example + [:div.demo-example.clearfix [:a.demo-example-hide {:on-click (fn [e] (.preventDefault e) (swap! showing not) @@ -48,6 +46,6 @@ [:div.simple-demo [comp]] [comp]))]) (when @showing - [:div.demo-source + [:div.demo-source.clearfix [:h3.demo-heading "Source"] src])]))) diff --git a/demo/reagentdemo/news.cljs b/demo/reagentdemo/news.cljs index 926b7ab..2204f9e 100644 --- a/demo/reagentdemo/news.cljs +++ b/demo/reagentdemo/news.cljs @@ -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)) @@ -81,7 +82,9 @@ [undo-demo-cleanup]]])) (defn main [] - [undo-example]) + [:div + [async/main] + [undo-example]]) (swap! page-map assoc "news/cloact-reagent-undo-demo.html" undo-example) diff --git a/demo/reagentdemo/news/async.cljs b/demo/reagentdemo/news/async.cljs new file mode 100644 index 0000000..a6abbce --- /dev/null +++ b/demo/reagentdemo/news/async.cljs @@ -0,0 +1,106 @@ +(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 (fn [] (reset! start-time (now)) nil) + stop #(reset! render-time (- (now) @start-time)) + timed-f (with-meta f + {:get-initial-state 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 (fn [x] + (str (if (< x 16) "0") + (-> x 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 (name color-part) " " (color-part @base-color) + [:input {:type "range" :min 0 :max 255 + :style {:width "100%"} + :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 + "number of colors " @ncolors + [:input {:type "range" :min 0 :max 500 + :style {:width "100%"} + :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 [] + [:div.reagent-demo + [title "Reagent: Faster by waiting"] + [:h1 [link {:href main} "Faster by waiting"]] + + [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) diff --git a/demo/reagentdemo/page.cljs b/demo/reagentdemo/page.cljs index eb1c244..7a3cb51 100644 --- a/demo/reagentdemo/page.cljs +++ b/demo/reagentdemo/page.cljs @@ -78,10 +78,16 @@ 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])) diff --git a/site/demo.css b/site/demo.css index e527fb1..be6dce3 100644 --- a/site/demo.css +++ b/site/demo.css @@ -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; @@ -139,4 +149,17 @@ ul.nav > li.brand > a { .demo-example-hide { float: right; cursor: pointer; +} + +/* Color demo */ + +.color-plate { + float: left; + height: 100px; + width: 100px; +} + +.color-samples { + clear: both; + padding-top: 0.5em; } \ No newline at end of file From 1b6b4e449e585a856aa51feaa94273c2fb6e4bc6 Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Sun, 2 Feb 2014 11:38:25 +0100 Subject: [PATCH 14/19] Make sure all React lifecycle callbacks are supported --- src/reagent/core.cljs | 2 ++ src/reagent/impl/component.cljs | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/reagent/core.cljs b/src/reagent/core.cljs index 9a99e92..3bbba72 100644 --- a/src/reagent/core.cljs +++ b/src/reagent/core.cljs @@ -44,7 +44,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])} diff --git a/src/reagent/impl/component.cljs b/src/reagent/impl/component.cljs index 10975fc..0b15bab 100644 --- a/src/reagent/impl/component.cljs +++ b/src/reagent/impl/component.cljs @@ -36,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))) @@ -152,11 +152,16 @@ ;; 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 From e10548f8ba8c64b12f0533b31989eea8601851d1 Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Sun, 2 Feb 2014 11:40:23 +0100 Subject: [PATCH 15/19] Reduce is great, and fast. Use it more --- src/reagent/impl/template.cljs | 8 ++++---- src/reagent/ratom.cljs | 15 ++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/reagent/impl/template.cljs b/src/reagent/impl/template.cljs index 8546a45..0db7ce3 100644 --- a/src/reagent/impl/template.cljs +++ b/src/reagent/impl/template.cljs @@ -76,10 +76,10 @@ objprops)))) (defn map-into-array [f arg coll] - (let [a (into-array coll)] - (dotimes [i (alength a)] - (aset a i (f (aget a i) arg))) - a)) + (reduce (fn [a x] + (doto a + (.push (f x arg)))) + #js [] coll)) (declare as-component) diff --git a/src/reagent/ratom.cljs b/src/reagent/ratom.cljs index 0c78003..415946e 100644 --- a/src/reagent/ratom.cljs +++ b/src/reagent/ratom.cljs @@ -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 From 1c702920bdaf5e7b4d614f742fb5ab9f571dc97f Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Mon, 3 Feb 2014 08:31:35 +0100 Subject: [PATCH 16/19] Start writing copy about async rendering --- demo/reagentdemo/news/async.cljs | 130 +++++++++++++++++++++++++------ site/demo.css | 8 ++ 2 files changed, 114 insertions(+), 24 deletions(-) diff --git a/demo/reagentdemo/news/async.cljs b/demo/reagentdemo/news/async.cljs index a6abbce..0ea22a8 100644 --- a/demo/reagentdemo/news/async.cljs +++ b/demo/reagentdemo/news/async.cljs @@ -12,10 +12,10 @@ (let [start-time (atom nil) render-time (atom nil) now #(.now js/Date) - start (fn [] (reset! start-time (now)) nil) + start #(reset! start-time (now)) stop #(reset! render-time (- (now) @start-time)) timed-f (with-meta f - {:get-initial-state start + {:component-will-mount start :component-will-update start :component-did-mount stop :component-did-update stop})] @@ -29,9 +29,8 @@ (def random-colors (atom nil)) (defn to-rgb [{:keys [red green blue]}] - (let [hex (fn [x] - (str (if (< x 16) "0") - (-> x js/Math.round (.toString 16))))] + (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]}] @@ -44,21 +43,19 @@ (repeatedly #(-> @base-color tweak-color to-rgb)))) (defn color-choose [{color-part :color-part}] - [:div (name color-part) " " (color-part @base-color) + [:div.color-slider + (name color-part) " " (color-part @base-color) [:input {:type "range" :min 0 :max 255 - :style {:width "100%"} :value (color-part @base-color) - :on-change - (fn [e] - (swap! base-color assoc - color-part (-> e .-target .-value int)) - (reset-random-colors))}]]) + :on-change (fn [e] + (swap! base-color assoc + color-part (-> e .-target .-value int)) + (reset-random-colors))}]]) (defn ncolors-choose [] - [:div - "number of colors " @ncolors + [:div.color-slider + "number of color divs " @ncolors [:input {:type "range" :min 0 :max 500 - :style {:width "100%"} :value @ncolors :on-change #(reset! ncolors (-> % .-target .-value))}]]) @@ -91,16 +88,101 @@ [timing-wrapper {:component-fn palette}]])) (defn main [] - [:div.reagent-demo - [title "Reagent: Faster by waiting"] - [:h1 [link {:href main} "Faster by waiting"]] + (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"] - [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])}]]) + [: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 " [:code "reagent.core/atom"] "s) are no longer + immediately rendered 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."] + + [: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, because 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 5ms 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) diff --git a/site/demo.css b/site/demo.css index be6dce3..6978958 100644 --- a/site/demo.css +++ b/site/demo.css @@ -122,6 +122,10 @@ ul.nav > li.brand > a { color: #444; } +.demo-text > ul > li { + margin-bottom: 1em; +} + .demo-example { background-color: #ebebeb; } @@ -159,6 +163,10 @@ ul.nav > li.brand > a { width: 100px; } +.color-slider > input { + width: 100%; +} + .color-samples { clear: both; padding-top: 0.5em; From 445f1da424c4371c62647fb8a6c488ae3d287418 Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Mon, 3 Feb 2014 10:20:05 +0100 Subject: [PATCH 17/19] Only show summaries on news page --- demo/reagentdemo/news.cljs | 37 +++---- demo/reagentdemo/news/async.cljs | 159 ++++++++++++++++--------------- demo/reagentdemo/page.cljs | 4 +- site/demo.css | 7 ++ 4 files changed, 115 insertions(+), 92 deletions(-) diff --git a/demo/reagentdemo/news.cljs b/demo/reagentdemo/news.cljs index 2204f9e..ad729df 100644 --- a/demo/reagentdemo/news.cljs +++ b/demo/reagentdemo/news.cljs @@ -45,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]] @@ -64,27 +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."] - - [undo-demo-cleanup]]])) + [:p "It simply saves the old state whenever it changes, and + restores it when the button is clicked."] + + [: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 [] [:div - [async/main] - [undo-example]]) + [async/main {:summary true}] + [undo-example {:summary true}]]) (swap! page-map assoc "news/cloact-reagent-undo-demo.html" undo-example) diff --git a/demo/reagentdemo/news/async.cljs b/demo/reagentdemo/news/async.cljs index 0ea22a8..c27f7b2 100644 --- a/demo/reagentdemo/news/async.cljs +++ b/demo/reagentdemo/news/async.cljs @@ -87,102 +87,111 @@ [ncolors-choose] [timing-wrapper {:component-fn palette}]])) -(defn main [] +(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"] + [: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 "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 " [:code "reagent.core/atom"] "s) are no longer - immediately rendered 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."] + [: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."] - [:p "This is good for all sorts of reasons:"] - [:ul + (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 "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 "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 "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, because Reagent uses requestAnimationFrame to do - the batching. That makes for example animations smoother."]] + [: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."]] + [: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"] + [: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 "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."] + [: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"] + [: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 "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 "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 "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 "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 "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 "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 5ms 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)."]] + [: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])}]])) + [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) diff --git a/demo/reagentdemo/page.cljs b/demo/reagentdemo/page.cljs index 7a3cb51..44e1cf0 100644 --- a/demo/reagentdemo/page.cljs +++ b/demo/reagentdemo/page.cljs @@ -74,7 +74,9 @@ :on-click (if history (fn [e] (.preventDefault e) - (reset! page href)) + (reset! page href) + (set! (.-scrollTop (.-body js/document)) + 0)) identity)) children))) diff --git a/site/demo.css b/site/demo.css index 6978958..e38c766 100644 --- a/site/demo.css +++ b/site/demo.css @@ -75,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 { @@ -155,6 +157,11 @@ ul.nav > li.brand > a { cursor: pointer; } +.news-read-more { + text-decoration: none; + font-size: 16px; +} + /* Color demo */ .color-plate { From e7f066177bf08143430ae6ac8a45cad2f5c22d21 Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Mon, 3 Feb 2014 13:58:31 +0100 Subject: [PATCH 18/19] Make sure changes to state in render fn don't cause disaster --- test/testcloact.cljs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/testcloact.cljs b/test/testcloact.cljs index 5e92df6..39191e0 100644 --- a/test/testcloact.cljs +++ b/test/testcloact.cljs @@ -220,6 +220,24 @@ (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)) From 046912a322fbee8d6b52fc27577b6dca018f750c Mon Sep 17 00:00:00 2001 From: Dan Holmsand Date: Mon, 3 Feb 2014 13:59:02 +0100 Subject: [PATCH 19/19] Add some doc strings --- src/reagent/core.cljs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/reagent/core.cljs b/src/reagent/core.cljs index 3bbba72..3ae07ab 100644 --- a/src/reagent/core.cljs +++ b/src/reagent/core.cljs @@ -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. @@ -108,7 +107,12 @@ specially, like React's transferPropsTo." [defaults props] (util/merge-props defaults props)) -(defn flush [] +(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)) @@ -125,7 +129,9 @@ re-rendered." ;; Utilities -(defn next-tick [f] +(defn next-tick + "Run f using requestAnimationFrame or equivalent." + [f] (comp/next-tick f)) (defn partial