├── resources
└── public
│ ├── js
│ └── .gitignore
│ ├── advanced.html
│ └── index.html
├── .gitignore
├── src
└── freactive
│ ├── plugins
│ ├── garden.cljs
│ └── goog_events.cljs
│ ├── animation.cljs
│ ├── react.cljs
│ ├── ui_common.cljs
│ └── dom.cljs
├── example
└── freactive
│ ├── elem_seqs.cljs
│ ├── test1.cljs
│ ├── diff_perf.cljs
│ └── dom_perf.cljs
├── project.clj
├── LICENSE
└── README.md
/resources/public/js/.gitignore:
--------------------------------------------------------------------------------
1 | compiled/
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | /classes
3 | /checkouts
4 | pom.xml
5 | pom.xml.asc
6 | *.jar
7 | *.class
8 | /.lein-*
9 | /.nrepl-port
10 | \#*\#
11 | *~
12 | .idea
13 | freactive.iml
14 | out
15 | .repl/
16 | figwheel_server.log
--------------------------------------------------------------------------------
/resources/public/advanced.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/freactive/plugins/garden.cljs:
--------------------------------------------------------------------------------
1 | (ns freactive.plugins.garden
2 | (:require
3 | [garden.types]
4 | [garden.compiler]
5 | [freactive.dom :as dom]))
6 |
7 |
8 | (extend-protocol dom/IDOMAttrValue
9 | garden.types.CSSUnit
10 | (-get-attr-value [this]
11 | (garden.compiler/render-css this)))
12 |
--------------------------------------------------------------------------------
/src/freactive/plugins/goog_events.cljs:
--------------------------------------------------------------------------------
1 | (ns freactive.plugins.goog-events
2 | (:require [goog.events :as events]
3 | [freactive.dom :as dom]))
4 |
5 | (defn use-goog-events!
6 | "Replaces freactive.dom's native DOM event handling with goog.events."
7 | []
8 | (set! dom/listen! events/listen)
9 | (set! dom/unlisten! events/unlisten))
10 |
--------------------------------------------------------------------------------
/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/example/freactive/elem_seqs.cljs:
--------------------------------------------------------------------------------
1 | (ns freactive.elem-seqs
2 | (:refer-clojure :exclude [atom])
3 | (:require
4 | [freactive.dom :as dom]
5 | [freactive.core :refer [atom cursor] :as r]
6 | [figwheel.client :as fw :include-macros true]
7 | [freactive.animation :as animation]
8 | [goog.string :as gstring]
9 | [cljs.core.async :refer [chan put! % :items keys))]
43 | [:div
44 | (rx (if (= @page 0)
45 | [:div "No bug"]
46 | [:div "Bug"
47 | (rx (into [:div] (mapv (fn [k] (item (cursor state [:items k]))) @item-keys)))]))
48 | [:div [:button {:on-click (fn [_] (swap! state update-in [:page] #(- 1 %)))} "toggle page"]]]))
49 |
50 | (dom/mount! (.getElementById js/document "root") (view))
51 |
52 | (fw/watch-and-reload)
53 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject freactive "0.2.0-SNAPSHOT"
2 | :description "High-performance, pure Clojurescript, declarative DOM library"
3 | :url "https://github.com/aaronc/freactive"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies [[org.clojure/data.avl "0.0.12"]
7 | [garden "1.2.5"]
8 | [freactive.core "0.2.0-SNAPSHOT"]
9 | [cljsjs/react "0.14.0-rc1-0"]]
10 | :profiles
11 | {:dev
12 | {:plugins [[lein-cljsbuild "1.0.5"]
13 | [lein-figwheel "0.3.3" :exclusions [org.cloure/clojure]]]
14 | :dependencies
15 | [[org.clojure/clojure "1.7.0"]
16 | [org.clojure/clojurescript "1.7.122"]
17 | [figwheel "0.3.3" :exclusions [org.clojure/clojure]]
18 | [org.clojure/core.async "0.1.346.0-17112a-alpha"]
19 | [bardo "0.1.0" :exclusions [org.clojure/clojure]]]
20 | :cljsbuild {:builds
21 | [{:id "example"
22 | :source-paths ["src" "example"
23 | "checkouts/freactive.core/src/clojure"
24 | "checkouts/freactive.core/test"]
25 | :compiler {:output-to "resources/public/js/compiled/app.js"
26 | :output-dir "resources/public/js/compiled/out"
27 | :optimizations :none
28 | :pretty-print true
29 | :source-map true}}
30 | {:id "example-advanced"
31 | :source-paths ["src" "example" "checkouts/freactive.core/src/clojure"]
32 | :compiler {:output-to "resources/public/js/compiled/advanced.js"
33 | :output-dir "resources/public/js/compiled/out-adv"
34 | :optimizations :advanced
35 | :pretty-print true
36 | :psuedo-names true
37 | :verbox true
38 | :main "freactive.dom-perf"}}]}}}
39 | :source-paths ["src"]
40 | :test-paths ["test" "example"])
41 |
--------------------------------------------------------------------------------
/src/freactive/animation.cljs:
--------------------------------------------------------------------------------
1 | (ns freactive.animation
2 | (:require
3 | [freactive.core :as r]
4 | [freactive.dom :as dom]
5 | [goog.object]))
6 |
7 | (deftype AnimationEaser [id state easing-fn animating on-complete
8 | watches fwatches]
9 | Object
10 | (rawDeref [this] state)
11 | (reactiveDeref [this]
12 | (r/register-dep this id r/fwatch-binding-info)
13 | state)
14 | (clean [_])
15 | (addFWatch [this key f]
16 | (when-not (aget (.-fwatches this) key)
17 | (set! (.-watchers this) (inc (.-watchers this)))
18 | (aset (.-fwatches this) key f)))
19 | (removeFWatch [this key]
20 | (when (aget (.-fwatches this) key)
21 | (set! (.-watchers this) (dec (.-watchers this)))
22 | (js-delete (.-fwatches this) key)))
23 | (notifyFWatches [this oldVal newVal]
24 | (goog.object/forEach
25 | (.-fwatches this)
26 | (fn [f key _]
27 | (f key this oldVal newVal)))
28 | (doseq [[key f] (.-watches this)]
29 | (f key this oldVal newVal)))
30 |
31 | r/IReactive
32 | (-get-binding-fns [this] r/fwatch-binding-info)
33 |
34 | IWatchable
35 | (-notify-watches [this oldval newval]
36 | (.notifyFWatches this oldval newval))
37 | (-add-watch [this key f]
38 | (set! (.-watches this) (assoc watches key f))
39 | this)
40 | (-remove-watch [this key]
41 | (set! (.-watches this) (dissoc watches key))
42 | this)
43 |
44 | IDeref
45 | (-deref [this] (.reactiveDeref this)))
46 |
47 | (defn easer [init-state]
48 | (AnimationEaser. (r/new-reactive-id) init-state nil false nil nil #js {}))
49 |
50 | (defn start-easing!
51 | ([easer from to duration easing-fn when-complete]
52 | (let [cur (.-state easer)
53 | ratio (/ (- to cur) (- to from))
54 | duration (* ratio duration)]
55 | (start-easing! easer to duration easing-fn when-complete)))
56 | ([easer to duration easing-fn when-complete]
57 | (let [start-ms (.rawDeref dom/frame-time)
58 | from (.-state easer)
59 | total-change (- to from)
60 | scaled-easing-fn
61 | (fn [new-ms]
62 | (let [t (/ (- new-ms start-ms) duration)
63 | t (if (>= t 1.0)
64 | (do
65 | (set! (.-animating easer) false)
66 | ;;(println "ending animation loop")
67 | 1.0)
68 | t)
69 | y (easing-fn t)]
70 | ;;(println "easing" t (.-animating easer))
71 | (+ from (* y total-change))))]
72 | (set! (.-easing-fn easer) scaled-easing-fn)
73 | (set! (.-on-complete easer) when-complete)
74 | (when-not (.-animating easer)
75 | ;;(println "starting animation loop")
76 | (set! (.-animating easer) true)
77 | (.addFWatch dom/frame-time (.-id easer)
78 | (fn [_ _ _ new-ms]
79 | (if (.-animating easer)
80 | (let [cur-state (.-state easer)
81 | new-state ((.-easing-fn easer) new-ms)]
82 | (set! (.-state easer) new-state)
83 | (.notifyFWatches easer cur-state new-state))
84 | (do
85 | (.removeFWatch dom/frame-time (.-id easer))
86 | (when-let [cb (.-on-complete easer)]
87 | (set! (.-on-complete easer) nil)
88 | (cb)))))))
89 | easer)))
90 |
91 | (defn easing-chain [easings]
92 | (if-let [easing-params (first easings)]
93 | (let [next-easing (easing-chain (rest easings))]
94 | (fn [callback]
95 | (apply start-easing!
96 | (conj easing-params (fn [] (next-easing callback))))))
97 | (fn [callback]
98 | (when callback
99 | (callback)))))
100 |
101 | (def linear identity)
102 |
103 | (defn quad-in [p] (* p p))
104 |
105 | (defn quad-out [p] (- (* p (- p 2))))
106 |
--------------------------------------------------------------------------------
/example/freactive/diff_perf.cljs:
--------------------------------------------------------------------------------
1 | (ns freactive.diff-perf
2 | (:refer-clojure :exclude [atom])
3 | (:require
4 | [freactive.dom :as dom]
5 | [freactive.core :refer [atom cursor] :as r]
6 | [figwheel.client :as fw :include-macros true])
7 | (:require-macros
8 | [freactive.macros :refer [rx debug-rx]]))
9 |
10 | (enable-console-print!)
11 | ;
12 | ;(dom/enable-fps-instrumentation!)
13 | ;
14 | (defn- get-window-width [] (.-innerWidth js/window))
15 |
16 | (defn- get-window-height [] (.-innerHeight js/window))
17 |
18 | (defonce width (atom (get-window-width)))
19 | ;
20 | (defonce height (atom (get-window-height)))
21 |
22 | (defonce mouse-x (atom (/ (get-window-width) 2)))
23 |
24 | (defonce mouse-y (atom (/ (get-window-height) 2)))
25 |
26 | (defn listen! [target name handler]
27 | (.addEventListener target name handler))
28 |
29 | (defonce init
30 | (do
31 | (listen! js/window "mousemove"
32 | (fn [e]
33 | (reset! mouse-x (.-clientX e))
34 | (reset! mouse-y (.-clientY e))))
35 |
36 | (listen! js/window "resize"
37 | (fn [e]
38 | (reset! width (get-window-width))
39 | (reset! height (get-window-height))))
40 |
41 | (listen! js/window "touchmove"
42 | (fn [e]
43 | (let [touches (.-touches e)]
44 | (when (= 1 (alength touches))
45 | (.preventDefault e)
46 | (let [touch (aget touches 0)]
47 | (reset! mouse-x (.-clientX touch))
48 | (reset! mouse-y (.-clientY touch)))))))))
49 |
50 | (defn circle [x y]
51 | (rx
52 | [:svg/circle {:cx @x :cy @y :r 2 :stroke "black" :fill "black"}]))
53 |
54 | (defonce n (atom 4))
55 | ;
56 | (defn- spacing-factor [n i]
57 | (let [i (inc i)
58 | n (inc n)
59 | x (/ i n)]
60 | (- 1 (.pow js/Math (- 1 x) 2))))
61 |
62 | (defn graph []
63 | (rx
64 | [:svg/svg
65 | {:width "100%" :height "100%"
66 | :style {:position "absolute" :left 0 :top "20px"}
67 | :viewBox (rx (str "0 20 " @width " " @height))}
68 | (circle mouse-x mouse-y)
69 | (let [n* @n
70 | spacer (partial spacing-factor n*)
71 | offsets (map spacer (range n*))
72 | lefts (vec (for [x offsets] (rx (* x @mouse-x))))
73 | rights (vec (for [x (reverse offsets)] (rx (let [w @width] (- w (* x (- w @mouse-x)))))))
74 | tops (vec (for [y offsets] (rx (* y @mouse-y))))
75 | bottoms (vec (for [y (reverse offsets)] (rx (let [h @height] (- h (* y (- h @mouse-y)))))))]
76 | [:svg/g
77 | (for [i (range n*)] (circle (nth lefts i) mouse-y))
78 | (for [i (range n*)] (circle (nth rights i) mouse-y))
79 | (for [j (range n*)] (circle mouse-x (nth tops j)))
80 | (for [j (range n*)] (circle mouse-x (nth bottoms j)))
81 | (for [i (range n*) j (range n*)] (circle (nth lefts i) (nth tops j)))
82 | (for [i (range n*) j (range n*)] (circle (nth lefts i) (nth bottoms j)))
83 | (for [i (range n*) j (range n*)] (circle (nth rights i) (nth tops j)))
84 | (for [i (range n*) j (range n*)] (circle (nth rights i) (nth bottoms j)))])]))
85 |
86 | (defn info []
87 | (rx
88 | (let [number-of-points
89 | (rx (let [n* @n n* (+ 1 (* 2 n*))] (* n* n*)))]
90 | [:span
91 | [:strong [:em [:a {:href "https://github.com/aaronc/freactive"} "freactive"]
92 | " pure diffing performance test."]
93 | "N = " (str @n) " "
94 | [:button {:on-click (fn [_] (swap! n dec))} "-"]
95 | [:button {:on-click (fn [_] (swap! n inc))} "+"]
96 | ", number of points = "
97 | (str @number-of-points)
98 | ", mouse at "
99 | (str @mouse-x ", " @mouse-y)
100 | ". "]]))
101 | )
102 |
103 | (defn view []
104 | [:div
105 | {:width "100%" :height "100%"}
106 | [:div
107 | {:width "100%"
108 | :style
109 | {:position "absolute" :left 0 :top 0 :height "12px"
110 | :font-size "12px"
111 | :font-family "sans-serif"}}
112 | (info)]
113 | (graph)])
114 |
115 | (dom/mount! (.getElementById js/document "root") (view))
116 |
117 |
118 | (fw/watch-and-reload)
119 |
--------------------------------------------------------------------------------
/src/freactive/react.cljs:
--------------------------------------------------------------------------------
1 | (ns freactive.react
2 | (:require
3 | [cljsjs.react]
4 | [freactive.core :as r]
5 | [freactive.dom :as dom]))
6 |
7 | (declare as-react)
8 |
9 | (defprotocol IAsReact
10 | (-as-react [x]))
11 |
12 | (extend-protocol IAsReact
13 | object
14 | (-as-react [x] (str x))
15 |
16 | nil
17 | (-as-react [x] "")
18 |
19 | boolean
20 | (-as-react [x] (str x))
21 |
22 | number
23 | (-as-react [x] (str x)))
24 |
25 | (defn init-binding [this new-ref]
26 | (.setState
27 | this
28 | #js {:binding
29 | (r/bind-attr*
30 | new-ref
31 | (fn [spec]
32 | (let [state (.-state this)]
33 | (when (or (not state) (not (identical? spec (.-spec state))))
34 | (.setState this #js {:spec spec}))))
35 | dom/queue-animation)}))
36 |
37 | (defn dispose-binding [this]
38 | (when-let [binding (.-binding (.-state this))]
39 | (r/dispose binding)))
40 |
41 | (def binding-spec
42 | #js
43 | {:displayName "FReactiveBinding"
44 | :componentWillMount
45 | (fn []
46 | (this-as
47 | this
48 | (init-binding
49 | this
50 | (if-let [fvec (.-fvec (.-props this))]
51 | (r/rx* (fn [] (apply (first fvec) (rest fvec))))
52 | (.-aref (.-props this))))))
53 | :componentDidMount
54 | (fn [])
55 | :componentWillReceiveProps
56 | (fn [next-props]
57 | (this-as
58 | this
59 | (or
60 | (if-let [fvec (.-fvec next-props)]
61 | (let [old-fvec (.-fvec (.-props this))]
62 | (when-not (= fvec old-fvec)
63 | (dispose-binding this)
64 | (init-binding this
65 | (r/rx* (fn [] (apply (first fvec) (rest fvec)))))))
66 | (let [old-ref (.-aref (.-props this))
67 | new-ref (.-aref next-props)]
68 | (when-not (identical? old-ref new-ref)
69 | (dispose-binding this)
70 | (init-binding this new-ref)))))))
71 | :shouldComponentUpdate
72 | (fn [next-props next-state]
73 | (this-as
74 | this
75 | (not
76 | (identical? (.-spec (.-state this))
77 | (.-spec next-state)))))
78 | :componentWillUpdate
79 | (fn [next-props next-state]
80 | (this-as
81 | this))
82 | :componentDidUpdate
83 | (fn [prev-props prev-state]
84 | (this-as
85 | this))
86 | :render
87 | (fn []
88 | (this-as
89 | this
90 | (as-react (.-spec (.-state this)))))
91 | :componentWillUnmount
92 | (fn []
93 | (this-as
94 | this
95 | (dispose-binding this)))})
96 |
97 | (def binding-class
98 | (.createClass js/React binding-spec))
99 |
100 | (defn resolve-react-class [uri tag] tag)
101 |
102 | (defn clj->react-attrs [attrs]
103 | ;; (let [js-attrs #js {}
104 | ;; class (:class attrs)
105 | ;; style (:style attrs)
106 | ;; attrs (dissoc attrs :class :style)]
107 | ;; (when class
108 | ;; (set! (.-className attrs) class))
109 | ;; (doseq [[k v] attrs]
110 | ;; ))
111 | (clj->js attrs)
112 | )
113 |
114 | (def ^:private element-ns-lookup #js {})
115 |
116 | (declare react-element)
117 |
118 | (defn- as-react [elem-spec]
119 | (cond
120 | (string? elem-spec)
121 | elem-spec
122 |
123 | (js/React.isValidElement elem-spec)
124 | elem-spec
125 |
126 | (sequential? elem-spec)
127 | (let [head (first elem-spec)]
128 | (cond
129 | (keyword? head)
130 | (let [tag-ns (namespace head)
131 | tag-name (name head)
132 | tail (rest elem-spec)]
133 | (if tag-ns
134 | (if-let [tag-handler (aget element-ns-lookup tag-ns)]
135 | (cond
136 | (fn? tag-handler)
137 | (as-react (tag-handler tag-name tail))
138 |
139 | :default
140 | (.warn js/console "Invalid ns node handler" tag-handler))
141 | (.warn js/console "Undefined ns node prefix" tag-ns))
142 | (react-element nil tag-name tail)))
143 |
144 | (ifn? head)
145 | (.createElement
146 | js/React binding-class (clj->js (assoc (meta elem-spec) :fvec elem-spec)) nil)))
147 |
148 | (satisfies? IDeref elem-spec)
149 | (.createElement
150 | js/React binding-class (clj->js (assoc (meta elem-spec) :aref elem-spec)) nil)
151 |
152 | :default
153 | (as-react (-as-react elem-spec))))
154 |
155 | (def react-element
156 | (dom/element*
157 | (fn [uri tag attrs children]
158 | (.createElement
159 | js/React
160 | (resolve-react-class uri tag)
161 | (clj->react-attrs attrs)
162 | children))
163 | (dom/append-children* as-react)))
164 |
165 | (defn render [element container]
166 | (.render js/React
167 | (as-react element)
168 | (if (dom/dom-node? container)
169 | container
170 | (dom/create-or-find-root-node container))))
171 |
--------------------------------------------------------------------------------
/example/freactive/dom_perf.cljs:
--------------------------------------------------------------------------------
1 | (ns freactive.dom-perf
2 | (:refer-clojure :exclude [atom])
3 | (:require
4 | [freactive.dom :as dom]
5 | [freactive.core :refer [atom cursor] :as r]
6 | [figwheel.client :as fw :include-macros true]
7 | [freactive.animation :as animation]
8 | [goog.string :as gstring]
9 | [cljs.core.async :refer [chan put! = idx 0) (< (dec (.-length elements))))
181 | (aget elements (inc idx)))))
182 |
183 | (deftype ElementSequence [elements velem-fn ^:mutable parent]
184 | Object
185 | (peek [this idx]
186 | (aget elements idx))
187 | (take [this idx]
188 | (let [elem (aget (.splice elements idx 1) 0)]
189 | (velem-take elem)
190 | elem))
191 | (count [this] (.-length elements))
192 | (move [this idx before-idx]
193 | (.insert this (aget (.splice elements idx 1) 0) before-idx))
194 | (insert [this elem before-idx]
195 | (let [elem (velem-fn elem)
196 | len (.-length elements)
197 | before-elem
198 | (if (and before-idx (< before-idx len))
199 | (aget elements before-idx)
200 | (velem-next-sibling-of parent this))]
201 | (.splice elements (or before-idx (alength elements)) 0 elem)
202 | (velem-insert elem parent before-elem))))
203 |
204 | (deftype ReactiveElementSequence [projection elements velem-fn enqueue-fn ^:mutable src ^:mutable parent]
205 | Object
206 | (dispose [this]
207 | (set! (.-disposed this) true)
208 | (r/dispose projection)
209 | (doseq [elem elements]
210 | (r/dispose elem)))
211 | (clear [this]
212 | (doseq [elem elements]
213 | (velem-remove elem)))
214 |
215 | r/IProjectionTarget
216 | (-target-peek [this idx]
217 | (aget elements idx))
218 | (-target-take [this idx]
219 | (let [elem (aget (.splice elements idx 1) 0)]
220 | (velem-take elem)
221 | elem))
222 | (-target-count [this] (.-length elements))
223 | (-target-move [this idx before-idx]
224 | (r/-target-insert this (aget (.splice elements idx 1) 0) before-idx))
225 | (-target-insert [this elem before-idx]
226 | (let [elem (velem-fn elem)
227 | len (.-length elements)
228 | before-elem
229 | (if (and before-idx (< before-idx len))
230 | (aget elements before-idx)
231 | (velem-next-sibling-of parent this))]
232 | (.splice elements (or before-idx (alength elements)) 0 elem)
233 | (velem-insert elem parent before-elem)))
234 |
235 |
236 | IVirtualElement
237 | (-velem-parent [this] parent)
238 | (-velem-native-element [this])
239 | (-velem-simple-element [this])
240 | (-velem-head [this]
241 | (when (> (.-length elements) 0)
242 | (velem-head (aget elements 0))))
243 | (-velem-next-sibling-of [this child]
244 | (array-next-sibling-of elements child))
245 | (-velem-insert [this vparent vnext-sibling]
246 | (set! parent vparent)
247 | (set! src (r/project projection this enqueue-fn velem-fn))
248 | this)
249 | (-velem-take [this]
250 | (doseq [elem elements]
251 | (velem-take elem)))
252 | (-velem-replace [this cur-velem]
253 | (let [vparent (velem-parent cur-velem)
254 | next-sib (velem-next-sibling-of vparent cur-velem)]
255 | (velem-remove cur-velem)
256 | (velem-insert this vparent next-sib)))
257 | (-velem-lifecycle-callback [this cb-name]))
258 |
259 | (defn reactive-element-sequence [projection velem-fn enqueue-fn]
260 | (ReactiveElementSequence. projection #js [] velem-fn enqueue-fn nil nil))
261 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC
2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM
3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
4 |
5 | 1. DEFINITIONS
6 |
7 | "Contribution" means:
8 |
9 | a) in the case of the initial Contributor, the initial code and
10 | documentation distributed under this Agreement, and
11 |
12 | b) in the case of each subsequent Contributor:
13 |
14 | i) changes to the Program, and
15 |
16 | ii) additions to the Program;
17 |
18 | where such changes and/or additions to the Program originate from and are
19 | distributed by that particular Contributor. A Contribution 'originates' from
20 | a Contributor if it was added to the Program by such Contributor itself or
21 | anyone acting on such Contributor's behalf. Contributions do not include
22 | additions to the Program which: (i) are separate modules of software
23 | distributed in conjunction with the Program under their own license
24 | agreement, and (ii) are not derivative works of the Program.
25 |
26 | "Contributor" means any person or entity that distributes the Program.
27 |
28 | "Licensed Patents" mean patent claims licensable by a Contributor which are
29 | necessarily infringed by the use or sale of its Contribution alone or when
30 | combined with the Program.
31 |
32 | "Program" means the Contributions distributed in accordance with this
33 | Agreement.
34 |
35 | "Recipient" means anyone who receives the Program under this Agreement,
36 | including all Contributors.
37 |
38 | 2. GRANT OF RIGHTS
39 |
40 | a) Subject to the terms of this Agreement, each Contributor hereby grants
41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to
42 | reproduce, prepare derivative works of, publicly display, publicly perform,
43 | distribute and sublicense the Contribution of such Contributor, if any, and
44 | such derivative works, in source code and object code form.
45 |
46 | b) Subject to the terms of this Agreement, each Contributor hereby grants
47 | Recipient a non-exclusive, worldwide, royalty-free patent license under
48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise
49 | transfer the Contribution of such Contributor, if any, in source code and
50 | object code form. This patent license shall apply to the combination of the
51 | Contribution and the Program if, at the time the Contribution is added by the
52 | Contributor, such addition of the Contribution causes such combination to be
53 | covered by the Licensed Patents. The patent license shall not apply to any
54 | other combinations which include the Contribution. No hardware per se is
55 | licensed hereunder.
56 |
57 | c) Recipient understands that although each Contributor grants the licenses
58 | to its Contributions set forth herein, no assurances are provided by any
59 | Contributor that the Program does not infringe the patent or other
60 | intellectual property rights of any other entity. Each Contributor disclaims
61 | any liability to Recipient for claims brought by any other entity based on
62 | infringement of intellectual property rights or otherwise. As a condition to
63 | exercising the rights and licenses granted hereunder, each Recipient hereby
64 | assumes sole responsibility to secure any other intellectual property rights
65 | needed, if any. For example, if a third party patent license is required to
66 | allow Recipient to distribute the Program, it is Recipient's responsibility
67 | to acquire that license before distributing the Program.
68 |
69 | d) Each Contributor represents that to its knowledge it has sufficient
70 | copyright rights in its Contribution, if any, to grant the copyright license
71 | set forth in this Agreement.
72 |
73 | 3. REQUIREMENTS
74 |
75 | A Contributor may choose to distribute the Program in object code form under
76 | its own license agreement, provided that:
77 |
78 | a) it complies with the terms and conditions of this Agreement; and
79 |
80 | b) its license agreement:
81 |
82 | i) effectively disclaims on behalf of all Contributors all warranties and
83 | conditions, express and implied, including warranties or conditions of title
84 | and non-infringement, and implied warranties or conditions of merchantability
85 | and fitness for a particular purpose;
86 |
87 | ii) effectively excludes on behalf of all Contributors all liability for
88 | damages, including direct, indirect, special, incidental and consequential
89 | damages, such as lost profits;
90 |
91 | iii) states that any provisions which differ from this Agreement are offered
92 | by that Contributor alone and not by any other party; and
93 |
94 | iv) states that source code for the Program is available from such
95 | Contributor, and informs licensees how to obtain it in a reasonable manner on
96 | or through a medium customarily used for software exchange.
97 |
98 | When the Program is made available in source code form:
99 |
100 | a) it must be made available under this Agreement; and
101 |
102 | b) a copy of this Agreement must be included with each copy of the Program.
103 |
104 | Contributors may not remove or alter any copyright notices contained within
105 | the Program.
106 |
107 | Each Contributor must identify itself as the originator of its Contribution,
108 | if any, in a manner that reasonably allows subsequent Recipients to identify
109 | the originator of the Contribution.
110 |
111 | 4. COMMERCIAL DISTRIBUTION
112 |
113 | Commercial distributors of software may accept certain responsibilities with
114 | respect to end users, business partners and the like. While this license is
115 | intended to facilitate the commercial use of the Program, the Contributor who
116 | includes the Program in a commercial product offering should do so in a
117 | manner which does not create potential liability for other Contributors.
118 | Therefore, if a Contributor includes the Program in a commercial product
119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend
120 | and indemnify every other Contributor ("Indemnified Contributor") against any
121 | losses, damages and costs (collectively "Losses") arising from claims,
122 | lawsuits and other legal actions brought by a third party against the
123 | Indemnified Contributor to the extent caused by the acts or omissions of such
124 | Commercial Contributor in connection with its distribution of the Program in
125 | a commercial product offering. The obligations in this section do not apply
126 | to any claims or Losses relating to any actual or alleged intellectual
127 | property infringement. In order to qualify, an Indemnified Contributor must:
128 | a) promptly notify the Commercial Contributor in writing of such claim, and
129 | b) allow the Commercial Contributor tocontrol, and cooperate with the
130 | Commercial Contributor in, the defense and any related settlement
131 | negotiations. The Indemnified Contributor may participate in any such claim
132 | at its own expense.
133 |
134 | For example, a Contributor might include the Program in a commercial product
135 | offering, Product X. That Contributor is then a Commercial Contributor. If
136 | that Commercial Contributor then makes performance claims, or offers
137 | warranties related to Product X, those performance claims and warranties are
138 | such Commercial Contributor's responsibility alone. Under this section, the
139 | Commercial Contributor would have to defend claims against the other
140 | Contributors related to those performance claims and warranties, and if a
141 | court requires any other Contributor to pay any damages as a result, the
142 | Commercial Contributor must pay those damages.
143 |
144 | 5. NO WARRANTY
145 |
146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON
147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER
148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR
149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A
150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the
151 | appropriateness of using and distributing the Program and assumes all risks
152 | associated with its exercise of rights under this Agreement , including but
153 | not limited to the risks and costs of program errors, compliance with
154 | applicable laws, damage to or loss of data, programs or equipment, and
155 | unavailability or interruption of operations.
156 |
157 | 6. DISCLAIMER OF LIABILITY
158 |
159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY
160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL,
161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION
162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE
165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY
166 | OF SUCH DAMAGES.
167 |
168 | 7. GENERAL
169 |
170 | If any provision of this Agreement is invalid or unenforceable under
171 | applicable law, it shall not affect the validity or enforceability of the
172 | remainder of the terms of this Agreement, and without further action by the
173 | parties hereto, such provision shall be reformed to the minimum extent
174 | necessary to make such provision valid and enforceable.
175 |
176 | If Recipient institutes patent litigation against any entity (including a
177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself
178 | (excluding combinations of the Program with other software or hardware)
179 | infringes such Recipient's patent(s), then such Recipient's rights granted
180 | under Section 2(b) shall terminate as of the date such litigation is filed.
181 |
182 | All Recipient's rights under this Agreement shall terminate if it fails to
183 | comply with any of the material terms or conditions of this Agreement and
184 | does not cure such failure in a reasonable period of time after becoming
185 | aware of such noncompliance. If all Recipient's rights under this Agreement
186 | terminate, Recipient agrees to cease use and distribution of the Program as
187 | soon as reasonably practicable. However, Recipient's obligations under this
188 | Agreement and any licenses granted by Recipient relating to the Program shall
189 | continue and survive.
190 |
191 | Everyone is permitted to copy and distribute copies of this Agreement, but in
192 | order to avoid inconsistency the Agreement is copyrighted and may only be
193 | modified in the following manner. The Agreement Steward reserves the right to
194 | publish new versions (including revisions) of this Agreement from time to
195 | time. No one other than the Agreement Steward has the right to modify this
196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The
197 | Eclipse Foundation may assign the responsibility to serve as the Agreement
198 | Steward to a suitable separate entity. Each new version of the Agreement will
199 | be given a distinguishing version number. The Program (including
200 | Contributions) may always be distributed subject to the version of the
201 | Agreement under which it was received. In addition, after a new version of
202 | the Agreement is published, Contributor may elect to distribute the Program
203 | (including its Contributions) under the new version. Except as expressly
204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or
205 | licenses to the intellectual property of any Contributor under this
206 | Agreement, whether expressly, by implication, estoppel or otherwise. All
207 | rights in the Program not expressly granted under this Agreement are
208 | reserved.
209 |
210 | This Agreement is governed by the laws of the State of Washington and the
211 | intellectual property laws of the United States of America. No party to this
212 | Agreement will bring a legal action under this Agreement more than one year
213 | after the cause of action arose. Each party waives its rights to a jury trial
214 | in any resulting litigation.
215 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # freactive
2 | *pronounced "f-reactive" for functional reactive - name subject to change. This library should be considered experimental - it has not been widely tested.*
3 |
4 | *FYI: this documentation is now significantly out-dated and will hopefully get updated soon*
5 |
6 | freactive is a high-performance, pure [Clojurescript](https://github.com/clojure/clojurescript), declarative DOM library. It uses [hiccup](https://github.com/weavejester/hiccup)-style syntax and Clojure's built-in deref and atom patterns. It is inspired by [reagent][reagent], [om][om] and [reflex][reflex] (as well as my experience with desktop GUI frameworks such as QML, JavaFX and WPF). **[See it in action!][dom-perf]**
7 |
8 | **Goals:**
9 | * Provide a **[simple, intuitive API](#hello-world)** that should be almost obvious to those familiar with Clojure (inspiration from [reagent][reagent])
10 | * Allow for **[high-performance](#performance)** rendering **[good enough for animated graphics][dom-perf]** based on a purely declarative syntax
11 | * Allow for **reactive binding of any attribute, style property or child node**
12 | * Allow for **coordinated management of state via [cursors](#cursors)** (inspiration from [om][om])
13 | * Provide **deeply-integrated [animation](#animations)** support
14 | * Allow for cursors based on paths as well as **lenses**
15 | * Provide a generic [items view component](#items-view) for **efficient viewing of large data sets**
16 | * **Minimize unnecessary triggering of update events**
17 | * Allow for binding of **any DOM tag including Polymer elements**
18 | * Coordinate all updates via **requestAnimationFrame** wherever possible
19 | * Use generic algorithms wherever possible for pluggable extension points including custom polyfills, event delegation, etc.
20 | * Be easy to [debug](#debugging-reactive-expressions)
21 | * Be written in **pure Clojurescript**
22 | * Provide support for older browsers via polyfills (not yet implemented)
23 |
24 | ## Two-minute tutorial
25 |
26 | **[Leiningen](http://leiningen.org) dependency info:**
27 |
28 | [](http://clojars.org/freactive)
29 |
30 | **Hello World example:**
31 |
32 | To try this quickly, you can install the [austin](https://github.com/cemerick/austin) repl plugin, run `austin-exec`, open a browser with the URL provided by austin and execute the code below. This code is also compatible with [lein-figwheel](https://github.com/bhauman/lein-figwheel) - this is possibly the best approach for live Clojurescript development available now.
33 |
34 | ```clojure
35 | (ns example1
36 | (:refer-clojure :exclude [atom])
37 | (:require [freactive.core :refer [atom cursor]]
38 | [freactive.dom :as dom])
39 | (:require-macros [freactive.macros :refer [rx]]))
40 |
41 | (defonce mouse-pos (atom nil))
42 |
43 | (defn view []
44 | [:div
45 | {:width "100%" :height "100%" :style {:border "1px solid black"}
46 | :on-mousemove (fn [e] (reset! mouse-pos [(.-clientX e) (.-clientY e)]))}
47 | [:h1 "Hello World!"]
48 | [:p "Your mouse is at: " (rx (str @mouse-pos))]])
49 |
50 | (defonce root (dom/append-child! (.-body js/document) [:div#root]))
51 |
52 | (dom/mount! root (view))
53 | ```
54 |
55 | **Explanation:**
56 |
57 | If you already understand [hiccup syntax](https://github.com/weavejester/hiccup#syntax) and Clojure's [`atom`](http://clojure.org/atoms), you're 90% of the way to understanding freactive. freactive's syntax is *very* similar to that of [reagent][reagent] with a few small differences.
58 |
59 | **Reactive atoms:** In freactive, instead of Clojure's atom, you use freactive's reactive `atom` which allows `deref`'s to be captured by an enclosing reactive expression - an `rx` in this case. (This is exactly the same idea as in [reagent][reagent] and I believe it originally came from [reflex][reflex]).
60 |
61 | **The `rx` macro**: The `rx` macro returns an `IDeref` instance (can be `deref`'ed with `@`) whose value is the body of the expression. This value gets updated when (and only when) one of the dependencies captured in its body (reactive `atom`s, other `rx`'s and also things like [`cursor`](#cursors)'s) gets "invalidated". (Pains were taken to make this invalidation process as efficient and configurable as possible.)
62 |
63 | **Binding to attributes, style properties and node positions:** Passing an `rx` or reactive `atom` (or any `IDeref` instance) as an attribute, style property or child of a DOM element represented via a hiccup vector binds it to that site. freactive makes sure that any updates to `rx`'s or `atom`'s are propogated directly to that DOM site only as often as necessary (coordinated with `requestAnimationFrame`).
64 |
65 | **Mounting components:** Components are mounted by passing a target node and hiccup vector to the `mount!` function (this will replace the last child of the target node with the mounted node!).
66 |
67 | **Events:** All attributes prefixed with `:on-` will treated as event handlers and take a Clojurescript function as an argument.
68 |
69 | **Helper functions:** A few additional helper functions such as - `append-child!`, `remove!`, and `listen!` - are included, but it is encouraged to use them sparingly and prefer the declarative hiccup syntax wherever possible.
70 |
71 | *Note: `atom` and `rx` are also available for Java Clojure and can be used with JavaFX via [fx-clj](https://github.com/aaronc/fx-clj) using a similar API. Originally this library was conceived as just a clj/cljs `atom` and `rx` library. After working with it in fx-clj, I wanted to do the same thing for the DOM and voila. Eventually a "core" library containing just the reactive data types will be split off. The Java version of core.clj is slightly out of sync with core.cljs. There is also an out-of-date ClojureCLR version.*
72 |
73 | ## Performance
74 |
75 | freactive should be able to handle fairly high performance graphics.
76 |
77 | Rather than saying how fast freactive does X compared to framework Y (which isn't always productive), I created an example that would really tax its ability to render. This is to give me (as well as potential library users) an idea of what it can and can't handle on different platforms.
78 |
79 | This example tries to animate points on the screen (SVG circle nodes) relative to the current mouse position. It has a complexity factor, `n`, which can be controlled by the `+` and `-` buttons. The number of points is *(2n + 1)2*.
80 |
81 | When you're observing the example, you can view the calculated FPS rate as well as the estimated number of DOM attributes updated per second. I recommend trying different values of `n` in different browsers (even try your phone!). Notice at which number of points the animation is and isn't smooth. Please report any issues you find here so we can make it better!: https://github.com/aaronc/freactive/issues.
82 |
83 | **Here is the example: http://aaronc.github.io/freactive/dom-perf**
84 |
85 | All of this is done declaratively with only the [syntax described above](#two-minute-tutorial), [easers](#easers) and [transitions](#transitions).
86 |
87 | **Here is the source for the example: https://github.com/aaronc/freactive/blob/master/test/freactive/dom_perf.cljs**
88 |
89 | This example benchmarks performance of reactive `atom`, `rx` and `easer` updates, freactive's rendering loop and applying those updates to DOM attributes and style properties. It also tests freactive's ability to clean up after itself and create new DOM elements. In the pause between transitions (usually not perceptable for small `n` values), freactive is cleaning up old elements (with attached `rx`'s that need to be deactivated) and creating new DOM elements. If the average frame rate for a given `n` doesn't drop after many transitions, it means that freactive is doing a good job of cleaning up after itself. If you notice a significant drop, please [report](issues) it!
90 |
91 | You should be able to see fairly smooth animations with thousands of points (n >= 16) on most modern computers even though the frame rate will start drop significantly. The "number of attrs updated" calculation is only valid when either the mouse is moving or a transition is happening.
92 |
93 | *(Okay... you may be wondering if I did a Reagent comparsion because the code is so similar. [Here it is](http://aaronc.github.io/freactive-reagent-comparison/). Reagent and React are quite fast! but freactive does seem to scale better for higher values of `n`. freactive also provides built-in animations.)*
94 |
95 | ## Transitions
96 |
97 | Transition callbacks can be added to any DOM element using the `with-transitions` function.
98 |
99 | ```clojure
100 | (with-transitions
101 | [:h1 "Hello World!"]
102 | {:on-show (fn [node callback]
103 | ;; do something
104 | (when callback (callback)))})
105 | ```
106 |
107 | The framework understands the `:on-show` and `:on-hide` transitions. These transitions will be applied upon changes at binding sites - i.e. at the site of an `rx` or an initial `mount!`. (A mechanism for triggering transitions based on changes to `data-state` is also planned.)
108 |
109 | ## Animations
110 |
111 | ### Easers
112 |
113 | *An API that wraps `easer` functionality in a convenient `animate!` function that takes style and attribute properties is planned.*
114 |
115 | `easer`'s are the basis of freactive animations. An easer is a specialized type of `deref` value that is updated at every animation frame based on an easing function and target and duration parameters. Essentially it provides "tween" values. Easers are defined with the `easer` function which takes an initial value. They can be transitioned to another value using the `start-easing!` function which takes the following parameters: `from` (optional), `to`, `duration`, `easing-fn` and a `on-complete` callback.
116 |
117 | An easer is designed to be used as a dependency in a reactive computation, like this:
118 |
119 | ```clojure
120 | (def ease-factor (animation/easer 0.0))
121 | (defn my-view []
122 | (dom/with-transitions
123 | [:h1 {:style
124 | {:font-size (rx (str (* 16 @ease-factor) "px"))}} "Hello World!"]
125 | {:on-show (fn [node callback]
126 | (animation/start-easing! ease-factor 0 1.0 1000
127 | (ease :quad-in-out) callback))}))
128 | ```
129 |
130 | By creating an `easing-chain`, we can do some more complex things:
131 | ```clojure
132 | (def ease1 (animation/easer 0.0))
133 | (def ease2 (animation/easer 1.0))
134 | (def complex-transition
135 | (animation/easing-chain
136 | [[ease1 0.0 1.0 1000 (ease :quad-in-out)]
137 | [ease2 1.0 0.0 500 (ease :quad-out)]
138 | [ease2 0.0 1.0 150 (ease :quint-in)]]))
139 | (defn my-view []
140 | (dom/with-transitions
141 | [:h1 {:style
142 | {:font-size (rx (str (* 16 @ease1) "px"))
143 | :opacity (rx (str @ease2))}}
144 | "Hello World!"]
145 | {:on-show
146 | (fn [node callback] (complex-transition callback))}))
147 | ```
148 |
149 | **Easing functions:** an easing function, `f`, is a function that is designed to take an input `t` parameter that ranges from `0.0` to `1.0` that has the property `(= (f 0) 0)` and `(= (f 1) 1)`. Basically the easing function is supposed to smoothly transition from `0` to `1`. The easer itself takes care of properly scaling the values based on `duration` and `from` and `to` values. The easing functions shown above use [bardo](https://github.com/pleasetrythisathome/bardo)'s `ease` function from `bardo.ease`. Any third-party easing library such as bardo can be used for easing functions. (freactive only provides the most basic `quad-in` and `quad-out` easing functions built-in.)
150 |
151 | **Optional `from` parameter:** the optional `from` parameter to `start-easing!` has a special behavior - if the current value of the easer is different from `from`, the `duration` of easing will be adjusted (linearly for now) based on the difference bettween `from` and the current value. This is to keep the speed of easing somewhat consistent. If you don't want this behavior and always want the same `duration` regardless of the current value of the easer, don't specify a `from` value.
152 |
153 | **Interupting in progress easings:** if `start-easing!` is called on an easer that is already in an easing transition that hasn't completed, it is equivalent to cancelling the current easing and sending the easer in a different direction starting from the current value. If there was on `on-complete` callback to the easing that was in progress it won't be called and is effectively "cancelled". (This behavior can be observed in the [performance example](#performance) if you click `+` or `-` while a transition is happening.)
154 |
155 | ## Cursors
156 |
157 | `cursor`'s in freactive behave and look exactly like `atom`'s. You can use Clojurescript's built-in `swap!` and `reset!` functions on them and state will be propogated back to their parents. By default, change notifications from the parent propagate to the cursor when and only when they affect the state of the cursor.
158 |
159 | Fundamentally, cursors are based on [lenses](https://speakerdeck.com/markhibberd/lens-from-the-ground-up-in-clojure). That means that you can pass any arbitrary getter (of the form `(fn [parent-state])`) and setter (of the form `(fn [parent-state cursor-state])`) and the cursor will handle it.
160 |
161 | ```clojure
162 | (def my-atom (atom 0))
163 | (defn print-number [my-atom-state]
164 | ;; print the number with some formmating
165 | )
166 | (defn parse-number [my-atom-state new-cursor-state]
167 | ;; parse new-cursor-state into a number and return it
168 | ;; if parsing fails you can just return my-atom-state
169 | ;; to cancel the update or throw a validation
170 | ;; exception
171 | )
172 | (def a-str (cursor my-atom print-number parse-number))
173 | ;; @a-str -> "0"
174 | (reset! a-str "1.2")
175 | (println @my-atom)
176 | ;; 1.2
177 | ```
178 |
179 | cursors can also be created by passing in a keyword or a key sequence that would be passed to `get-in` or `assoc-in` to the `cursor` function:
180 |
181 | ```clojure
182 | (def my-atom (atom {:a {:b [{:x 0}]}}))
183 | (def ab0 (cursor my-atom [:a :b 0])) ;; @ab0 -> {:x 0}
184 | (def a (cursor my-atom :a) ;; a keyword can be used as well
185 | ```
186 |
187 | This is somewhat similar (but not exactly) to cursors in [om][om] - which was the inspiration for cursors in freactive. It should be noted that in freactive, cursors were designed to work with lenses first and then with key or key sequences (`korks`) for convenience. A cursor doesn't know anything about the structure of data it references (i.e. the associative path from parent to child).
188 |
189 | ## Items View
190 |
191 | An experimental `items-view` has been created, but is still a work in progress... This documentation describes the concept in a very general way.
192 |
193 | The idea of the `items-view` is to provide a generic container for large collections of objects that send notifications about exactly which items changed so that diffing is not neeeded. An analogue to this in the desktop UI world is the [WPF `ItemsControl`](http://msdn.microsoft.com/en-us/library/system.windows.controls.itemscontrol%28v=vs.110%29.aspx) which is a base class for `ListView`'s and `TreeView`'s. Basically it allows to framework to observe a collection and add or remove nodes rendering them based on a data template.
194 |
195 | In freactive, the `items-view` will take a `container` parameter (hiccup virtual node), a `template-fn` which will receive a single argument representing a `cursor` to a single item in the collection and should return a hiccup virtual node (can be reactive), and a `collection` parameter representing the underlying collection that is being rendered. The `collection` should be satisfy the `IObservableCollection` protocol which is still being fleshed out. A basic implementation of `IObservableCollection` will be provided which wraps an atom containing a Clojure map or vector and provides specific methods for adding/updating/removing individual items so that notifications can be done on an item-specific basis with no need for diffing. This type of idiom will allow for quite large collections. `IObservableCollection` could eventually be extended to support a database-backed collection and then we have something like Meteor in Clojurescript..! The `items-view` should provide built-in support for applying and removing sorts in a stable way, for adding and removing filters and for limiting the displayed elements to a specific range (to support paging and infinite scrolling). It should be agnostic to the underlying `IObservableCollection` as well as the `container` and `template-fn` and do things as generically as possible.
196 |
197 | It should be noted that the `items-view` will be orthogonal to the other functionality in freactive - freactive "core" will just attempt to provide idioms which would support an `items-view` and a good out of the box implementation. In reality, `items-view` could be a separate library and alternate implementations could co-exist.
198 |
199 | ## Debugging Reactive Expressions
200 |
201 | Reactive expressions can be hard to debug - sometimes we notice that something should be getting invalidated that isn't or it seems like something is getting updated too often.
202 |
203 | The `debug-rx` macro can be placed around the initialization of any `rx`:
204 | ```clojure
205 | (debug-rx (rx (str @n)))
206 | ```
207 |
208 | and you should seeing verbose debug statements corresponding to:
209 | * start of dependency capture
210 | * each dependency capture
211 | * each invalidation event with a print out of watch keys (note: not all watches aware of this `rx` may be registered - part of freactive's optimizations are smart attaching and removing of watches based on dirty flags)
212 |
213 | ## Reactive Change Notifications In-depth
214 |
215 | ### Differences between regular atoms and reactive atoms
216 |
217 | In addition to their ability to register themselves as a dependency to an `rx`, reactive `atom`'s have one additional difference from regular `atom`s. Reactive `atom`s do an equality check (using `=`) before completing a change and notifying watches. i.e: they will only report a change when the value actually has changed.
218 |
219 | ### Eagerness and Laziness
220 |
221 | A lazy dependency invalidates a parent `rx` whenever it gets invalidated (but it doesn't check to see if its value has really changed). An eager dependency checks to see if it really has changed before notifying its parent. *By default, `rx`'s are lazy and `cursor`'s are eager*. This is because an `rx` is something whose value almost always changes whenever a dependency changes and a `cursor` is usually something that will only change when its portion of a larger state changes (i.e. the path `[:a :b]` in `{:a {:b 1} c: {:d 2}}`). There is also an `eager-rx` and a `lazy-cursor` if you want to invert this default behavior.
222 |
223 | **Details:**
224 |
225 | *You probably shouldn't need to understand this to develop most apps, but it may be useful to those trying to maximize performance in complex situations.*
226 |
227 | Laziness relates to the `IInvalidates` protocol which freactive introduces. Basically `IInvalidates` defines three protocol methods: `-add-invalidation-watch`, `-remove-invalidation-watch` and `-notify-invalidations-watches`. These take the same parameters as the corresponding `-add-watch`, `-remove-watch`, etc. methods from `IWatchable`. The only difference is that the callback function should take 2 args (instead of 4): `key` and `ref`. When something is invalidated, we are communicating that it's state will probably change, but that we don't know what the new state is yet!
228 |
229 | So, if we register an `invalidation-watch` against an `rx`, it will perform lazily - i.e. it will only compute its new state when we `deref` it. If we register a `watch` against it, it will perform eagerly and it will only notify about state changes (to both watches and invalidation watches) if the value has actually change!
230 |
231 | So, how do we make an `rx` or `cursor` lazy or eager? Well, it may seem counter-intuitive, but reactives in freactive actually register their own dependencies. Instead of "registering" with the parent, they look for a `*invalidate-rx*` function to be bound in the current context and they they register it either as a watch or invalidation watch. `*invalidate-rx*` actually should be a `fn` with 0, 2 and 4 arity overloads. The 0 arity-overload allows the dependency to take entire control over the invalidation process and the 2 and 4 arities correspond to invalidation watches and watches respectively. Whenever `*invalidate-rx*` is called, it immediately removes the watch on whatever dependency called it (it will be added the next time the `rx` is `deref`ed if it is still in the `rx` scope.) After some benchmarking, this method seemed to be the most efficient general method.
232 |
233 | Anyway, because dependencies register themselves, they can decide whether to register themselves lazily or eagerly. It should be noted that a dependency is eager whenever anything registers a `watch` (as opposed to an `invalidation-watch`) against it. We can override a dependency's choice of eagerness or laziness by `deref`ing them within an `eagerly` or `lazily` macro - if you ever should need that: `(eagerly @a)`.
234 |
235 | **Propogation of changes to the DOM:**
236 |
237 | Attribute and node change listeners always try to register an `invalidation-watch` first and when not available (for atoms for instance) a `watch`. Whenenver they receive a change notification, they remove the watch, queue an update to the render queue, and add the watch again right before the update is applied (when `deref` is called).
238 |
239 | ### Deciding not to register a reactive dependency
240 |
241 | Sometimes you want to reference a reactive `atom` or `rx` from within an `rx` without registering it as a dependency! Or maybe you want to make sure that in library code, no surrounding `rx` is calling your function. The `non-reactively` macro (in `freactive.macros`) will take care of this:
242 | ```clojure
243 | ;; a is registered as a dependency, but b isn't!
244 | (rx (+ @a (non-reactively @b)))
245 | ```
246 |
247 | ## Contributions
248 |
249 | **Contributions (including pull requests) are welcome!**
250 |
251 | ### TODO list
252 |
253 | **If you would like to contribute, here is a list of things that would help get this library to a mature state.** The list is organized by category and relative priority. Each item has a link to an issue which you can comment on, assign to yourself possibly, etc.
254 |
255 | Core functionality:
256 | * [Good polyfills for things like `requestAnimationFrame`, `addEventListener`, etc. to support older browsers where feasible](https://github.com/aaronc/freactive/issues/4)
257 | * [Benchmarking of event handlers - do we need to do something like React's synthentic events?](https://github.com/aaronc/freactive/issues/6)
258 |
259 | Items view:
260 | * [Efficient algorithms for applying stable (possible in place) sorting to the `items-view`](https://github.com/aaronc/freactive/issues/5)
261 |
262 | Animations:
263 | * [A stable (possibly 3rd party) easings library](https://github.com/aaronc/freactive/issues/7). I incorporated some easings from [ominate](https://github.com/danielytics/ominate) - it has some open bug reports - maybe those can be fixed and the easings part can be forked so that it's shared. There's also [tween-clj](https://github.com/gstamp/tween-clj).
264 |
265 | More examples and testing on different platforms is always welcome.
266 |
267 | Comments, suggestions and questions can be posted here: https://github.com/aaronc/freactive/issues
268 |
269 | ## License
270 |
271 | Distributed under the Eclipse Public License, either version 1.0 or (at your option) any later version.
272 |
273 | [dom-perf]: http://aaronc.github.io/freactive/dom-perf
274 | [issues]: https://github.com/aaronc/freactive/issues
275 | [reagent]: https://github.com/reagent-project/reagent
276 | [om]: https://github.com/swannodette/om
277 | [reflex]: https://github.com/lynaghk/reflex
278 |
--------------------------------------------------------------------------------
/src/freactive/dom.cljs:
--------------------------------------------------------------------------------
1 | (ns freactive.dom
2 | (:require
3 | [freactive.core :as r]
4 | [freactive.ui-common :as ui]
5 | [goog.object]))
6 |
7 | ;; ## Polyfills
8 |
9 | (def request-animation-frame
10 | (or
11 | (.-requestAnimationFrame js/window)
12 | (.-webkitRequestAnimationFrame js/window)
13 | (.-mozRequestAnimationFrame js/window)
14 | (.-msRequestAnimationFrame js/window)
15 | (.-oRequestAnimationFrame js/window)
16 | (let [t0 (.getTime (js/Date.))]
17 | (fn [f]
18 | (js/setTimeout
19 | #(f (- (.getTime (js/Date.)) t0))
20 | 16.66666)))))
21 |
22 | ;; Render Loop
23 |
24 | (defonce ^:private render-queue #js [])
25 |
26 | (def ^:private enable-fps-instrumentation false)
27 |
28 | (defn enable-fps-instrumentation!
29 | ([] (enable-fps-instrumentation! true))
30 | ([enable] (set! enable-fps-instrumentation enable)))
31 |
32 | (def ^:private instrumentation-i -1)
33 |
34 | (def ^:private last-instrumentation-time)
35 |
36 | (defonce fps (r/atom nil))
37 |
38 | (defonce frame-time (r/atom nil))
39 |
40 | (def ^:private ^:dynamic *animating* nil)
41 |
42 | (def ^:private animation-requested false)
43 |
44 | (declare render-fn)
45 |
46 | (defn- request-render []
47 | (when-not animation-requested
48 | (set! animation-requested true)
49 | (request-animation-frame render-fn)))
50 |
51 | (defn- render-fn [frame-ms]
52 | (binding [*animating* true]
53 | (set! animation-requested false)
54 | (reset! frame-time frame-ms)
55 | (when enable-fps-instrumentation
56 | (if (identical? instrumentation-i 14)
57 | (do
58 | (reset! fps (* 1000 (/ 15 (- frame-ms last-instrumentation-time))))
59 | (set! instrumentation-i 0))
60 | (set! instrumentation-i (inc instrumentation-i)))
61 | (when (identical? 0 instrumentation-i)
62 | (set! last-instrumentation-time frame-ms)))
63 | (let [queue render-queue
64 | n (alength queue)]
65 | (when (> n 0)
66 | (set! render-queue #js [])
67 | (loop [i 0]
68 | (when (< i n)
69 | ((aget queue i))
70 | (recur (inc i)))))))
71 | (when (or enable-fps-instrumentation (> (.-watchers frame-time) 0))
72 | (request-render)))
73 |
74 | (defn queue-animation [f]
75 | (if *animating*
76 | (f)
77 | (do
78 | (.push render-queue f)
79 | (request-render))))
80 |
81 | ;; ## Attributes, Styles & Events
82 |
83 | ;; TODO lifecycle callbacks
84 |
85 | ;; (defn- on-moving [state node cb]
86 | ;; (if-let [node-moving (get-state-attr state ":node/on-moving")]
87 | ;; (node-moving node cb)
88 | ;; (cb)))
89 |
90 | ;; (defn- on-moved [state node]
91 | ;; (when-let [node-moved (get-state-attr state ":node/on-moved")]
92 | ;; (node-moved node)))
93 |
94 | (defn update-attrs
95 | "Convenience function to update the attrs in a virtual dom vector.
96 | Works like Clojure's update function but f (and its args) only modify the attrs
97 | map in velem."
98 | ([velem f & args]
99 | (let [tag (first velem)
100 | attrs? (second velem)
101 | attrs (when (map? attrs?) attrs?)]
102 | (concat [tag (apply f attrs args)] (if attrs (nnext velem) (next velem))))))
103 |
104 | ;; Plugin Support
105 |
106 | (def ^:private node-ns-lookup
107 | #js
108 | {"svg" "http://www.w3.org/2000/svg"})
109 |
110 | (defn register-node-prefix! [prefix xml-ns-or-handler]
111 | (aset node-ns-lookup (name prefix) xml-ns-or-handler))
112 |
113 | (def ^:private attr-ns-lookup
114 | #js
115 | {"node" (fn [_ _ _])
116 | "state" (fn [_ _ _])})
117 |
118 | (defn register-attr-prefix! [prefix xml-ns-or-handler]
119 | (aset attr-ns-lookup (name prefix) xml-ns-or-handler))
120 |
121 | ;; Core DOM stuff
122 |
123 | (defn- get-element-state [x]
124 | (.-freactive-state x))
125 |
126 | (defn- dom-insert [parent dom-node vnext-sibling]
127 | (let [dom-parent (ui/native-parent parent)]
128 | ;; (when vnext-sibling (println "vnext" (type vnext-sibling)))
129 | (if-let [dom-before (ui/next-native-sibling vnext-sibling)]
130 | (do
131 | ;; (println "insert" (goog/getUid dom-node)
132 | ;; "dom-parent" dom-parent (goog/getUid dom-parent)
133 | ;; "dom-before" dom-before (goog/getUid dom-before))
134 | (.insertBefore dom-parent dom-node dom-before))
135 | (do
136 | ;; (println "append" (goog/getUid dom-node)
137 | ;; "dom-parent" dom-parent (goog/getUid dom-parent))
138 | (.appendChild dom-parent dom-node)))))
139 |
140 | (defn- dom-remove [dom-node]
141 | (when-let [parent (.-parentNode dom-node)]
142 | (.removeChild parent dom-node)))
143 |
144 | (defn- dom-simple-replace [new-node old-node]
145 | (when-let [parent (.-parentNode old-node)]
146 | (.replaceChild parent new-node old-node)))
147 |
148 | (defn- dom-remove-replace [new-node old-velem]
149 | (let [parent (ui/velem-parent old-velem)
150 | next-sib (ui/velem-next-sibling-of parent old-velem)]
151 | (ui/velem-remove old-velem)
152 | (dom-insert (ui/native-parent parent) new-node next-sib)))
153 |
154 | (deftype UnmanagedDOMNode [node on-dispose ^:mutable parent]
155 | Object
156 | (dispose [this]
157 | (when on-dispose
158 | (on-dispose this)))
159 |
160 | ui/IVirtualElement
161 | (-velem-parent [this] parent)
162 | (-velem-head [this] this)
163 | (-velem-next-sibling-of [this child])
164 | (-velem-native-element [this] node)
165 | (-velem-simple-element [this] this)
166 | (-velem-insert [this vparent vnext-sibling]
167 | (set! parent vparent)
168 | (dom-insert vparent node vnext-sibling)
169 | this)
170 | (-velem-take [this]
171 | (dom-remove node))
172 | (-velem-replace [this old-velem]
173 | (if-let [old-node (ui/velem-native-element old-velem)]
174 | (dom-simple-replace node old-node)
175 | (dom-remove-replace node old-velem))
176 | this)
177 | (-velem-lifecycle-callback [this cb-name]))
178 |
179 | ;; Attribute Map Stuff
180 |
181 | (defn- ->str [kw-or-str]
182 | (if (string? kw-or-str)
183 | kw-or-str
184 | (if-let [attr-ns (namespace kw-or-str)]
185 | (str attr-ns "/" (name kw-or-str))
186 | (name kw-or-str))))
187 |
188 | (defn- dispose-states [states]
189 | )
190 |
191 | (defn- attr-diff* [node oas attr-map bind-attr]
192 | (let [nas #js {}]
193 | (doseq [[k v] attr-map]
194 | (let [kstr (->str k)]
195 | (if-let [existing (when oas (aget oas kstr))]
196 | (do
197 | (js-delete oas kstr)
198 | (when-let [new-state (existing v)]
199 | (aset nas kstr new-state)))
200 | (aset nas kstr (bind-attr node k v)))))
201 | (dispose-states oas)
202 | nas))
203 |
204 | (deftype AttrMapBinder [bind-fn clean-fn element ^:mutable states]
205 | IFn
206 | (-invoke [this new-value]
207 | (set! states (attr-diff* element states new-value bind-fn))
208 | this)
209 | Object
210 | (clean [this]
211 | (when clean-fn (clean-fn))
212 | (when states
213 | (goog.object/forEach
214 | states
215 | (fn [state k _]
216 | (when state
217 | (when (.-clean state)
218 | (.clean state)))))))
219 | (dispose [this]
220 | (when states
221 | (goog.object/forEach
222 | states
223 | (fn [state k _]
224 | (when state
225 | (r/dispose state)))))))
226 |
227 | (defn- wrap-attr-map-binder
228 | ([bind-fn]
229 | (wrap-attr-map-binder bind-fn nil))
230 | ([bind-fn clean-fn]
231 | (fn [element value]
232 | ((AttrMapBinder. bind-fn clean-fn element nil) value))))
233 |
234 | ;; Managed DOMElement stuff
235 |
236 | (defprotocol IDOMAttrValue
237 | (-get-attr-value [value]))
238 |
239 | (defn- normalize-attr-value [value]
240 | (cond
241 | (.-substring value) value
242 | (keyword? value) (name value)
243 | (identical? value true) ""
244 | (identical? value false) nil
245 | (satisfies? IDOMAttrValue value) (-get-attr-value value)
246 | :default (.toString value)))
247 |
248 | (defn- set-attr! [element attr-name attr-value]
249 | ;; (println "setting attr" element attr-name attr-value)
250 | (if attr-value
251 | (.setAttribute element attr-name (normalize-attr-value attr-value))
252 | (.removeAttribute element attr-name)))
253 |
254 | (defn- set-style! [elem prop-name prop-value]
255 | ;(println "set-style-prop!" elem prop-name prop-value)
256 | (if prop-value
257 | (aset (.-style elem) prop-name (normalize-attr-value prop-value))
258 | (js-delete (.-style elem) prop-name))
259 | prop-value)
260 |
261 | (defn ^:dynamic ^:pluggable listen!
262 | "Adds an event handler. Can be replaced by a plugin such as goog.events."
263 | [element evt-name handler]
264 | (.addEventListener element evt-name handler false))
265 |
266 | (defn ^:dynamic ^:pluggable unlisten!
267 | "Removes an event handler. Can be replaced by a plugin such as goog.events."
268 | [element evt-name handler]
269 | (.removeEventListener element evt-name handler))
270 |
271 | (deftype EventBinding [node event-name ^:mutable handler]
272 | IFn
273 | (-invoke [this new-handler]
274 | (when-not (identical? new-handler handler)
275 | (.unlisten this)
276 | (set! handler new-handler)
277 | (when handler
278 | (listen! node event-name handler)))
279 | this)
280 | Object
281 | (unlisten [this]
282 | (when handler
283 | (unlisten! node event-name handler)))
284 | (dispose [this]
285 | (.unlisten this)))
286 |
287 | (defn- bind-event! [node event-name handler]
288 | ((EventBinding. node (name event-name) nil) handler))
289 |
290 | (defn- do-bind-attr [setter val]
291 | (let [binding
292 | (r/bind-attr* val setter queue-animation)]
293 | binding))
294 |
295 | (defn- bind-style! [node style-kw style-val]
296 | (let [style-name (name style-kw)]
297 | (do-bind-attr (fn [val] (set-style! node style-name val)) style-val)))
298 |
299 | (defn- do-set-data-state! [element state]
300 | (set-attr! element "data-state" state))
301 |
302 | (defn- get-data-state [element]
303 | (.getAttribute element "data-state"))
304 |
305 | (defn- get-state-attr [state attr-str]
306 | (when-let [attrs (when state (.-attrs state))]
307 | (aget attrs attr-str)))
308 |
309 | (defn- set-data-state!
310 | ([element state]
311 | (let [cur-state (get-data-state element)
312 | node-state (get-element-state element)
313 | state (when state (name state))]
314 | (when-not (identical? cur-state state)
315 | (do-set-data-state! element state)
316 | (when-let [enter-transition (get-state-attr node-state (str ":state/on-" state))]
317 | (enter-transition element cur-state state))))))
318 |
319 | (def ^:private attr-setters
320 | #js
321 | {:data-state (fn [element state]
322 | (set-data-state! element state)
323 | state)
324 | :class (fn [element cls]
325 | (set! (.-className element) (if (nil? cls) "" cls))
326 | cls)})
327 |
328 | ;; attributes to set directly
329 | (doseq [a #js ["id" "value"]]
330 | (aset attr-setters a
331 | (fn [e v]
332 | (aset e a v))))
333 |
334 | (defn- get-attr-setter [element attr-name]
335 | (if-let [setter (aget attr-setters attr-name)]
336 | (fn [attr-value] (setter element attr-value))
337 | (fn [attr-value]
338 | (set-attr! element attr-name attr-value))))
339 |
340 | (defn- get-ns-attr-setter [element attr-ns attr-name]
341 | (fn [attr-value]
342 | (if attr-value
343 | (.setAttributeNS element attr-ns attr-name
344 | (normalize-attr-value attr-value))
345 | (.removeAttributeNS element attr-ns attr-name))
346 | attr-value))
347 |
348 | (def ^:private special-attrs
349 | #js {"events" (wrap-attr-map-binder bind-event!)
350 | "style" (wrap-attr-map-binder bind-style!)})
351 |
352 | (defn- bind-attr! [element attr-key attr-value]
353 | (let [attr-ns (namespace attr-key)
354 | attr-name (name attr-key)]
355 | (if attr-ns
356 | (if-let [attr-handler (aget attr-ns-lookup attr-ns)]
357 | (cond
358 | (string? attr-handler)
359 | (do-bind-attr (get-ns-attr-setter element attr-ns attr-name) attr-value)
360 |
361 | (ifn? attr-handler)
362 | (attr-handler element attr-name attr-value)
363 |
364 | :default
365 | (.warn js/console "Invalid ns attr handler" attr-handler))
366 | (.warn js/console "Undefined ns attr prefix" attr-ns))
367 | (if-let [special (aget special-attrs attr-name)]
368 | (special element attr-value)
369 | (cond
370 | (identical? 0 (.indexOf attr-name "on-"))
371 | (bind-event! element (.substring attr-name 3) attr-value)
372 |
373 | :default
374 | (do-bind-attr (get-attr-setter element attr-name) attr-value))))))
375 |
376 | (def ^:private bind-attrs! (wrap-attr-map-binder bind-attr!))
377 |
378 | (deftype DOMElement [ns-uri tag ^:mutable attrs children ^:mutable node
379 | ^:mutable parent ^:mutable attr-binder]
380 | IHash
381 | (-hash [this] (goog/getUid this))
382 |
383 | Object
384 | (dispose [this]
385 | (r/dispose attr-binder)
386 | (doseq [i (range (.-length children))]
387 | (r/dispose (aget children i))))
388 | (ensureNode [this]
389 | (when-not node
390 | (set! node
391 | (if ns-uri
392 | (.createElementNS js/document ns-uri tag)
393 | (.createElement js/document tag)))
394 | (set! (.-freactive-state node) this)
395 | (set! attr-binder (bind-attrs! node attrs))
396 | (doseq [child children]
397 | (ui/velem-insert child this nil))))
398 | (onAttached [this]
399 | (when-let [on-attached (get attrs :node/on-attached)]
400 | (on-attached node)))
401 | (updateAttrs [this new-attrs]
402 | (set! attr-binder (attr-binder node new-attrs))
403 | (set! attrs new-attrs))
404 |
405 | ui/IVirtualElement
406 | (-velem-parent [this] parent)
407 | (-velem-head [this] this)
408 | (-velem-next-sibling-of [this child]
409 | (ui/array-next-sibling-of children child))
410 | (-velem-native-element [this] node)
411 | (-velem-simple-element [this] this)
412 | (-velem-insert [this vparent vnext-sibling]
413 | (set! parent vparent)
414 | (.ensureNode this)
415 | (dom-insert parent node vnext-sibling)
416 | (.onAttached this)
417 | this)
418 | (-velem-take [this]
419 | (dom-remove node))
420 | (-velem-replace [this old-velem]
421 | (.ensureNode this)
422 | (if-let [old-node (ui/velem-native-element old-velem)]
423 | (do
424 | (dom-simple-replace node old-node)
425 | (r/dispose old-velem))
426 | (dom-remove-replace node old-velem))
427 | (.onAttached this)
428 | this)
429 | (-velem-lifecycle-callback [this cb-name]
430 | (get attrs cb-name)))
431 |
432 | (defn- text-node? [dom-node]
433 | (identical? (.-nodeType dom-node) 3))
434 |
435 | (deftype DOMTextNode [^:mutable text ^:mutable node ^:mutable parent]
436 | Object
437 | (ensureNode [this]
438 | (when-not node
439 | (set! node (.createTextNode js/document text))))
440 | ui/IVirtualElement
441 | (-velem-parent [this] parent)
442 | (-velem-head [this] this)
443 | (-velem-next-sibling-of [this child])
444 | (-velem-native-element [this]
445 | (.ensureNode this)
446 | node)
447 | (-velem-simple-element [this] this)
448 | (-velem-insert [this vparent vnext-sibling]
449 | (.ensureNode this)
450 | (set! parent vparent)
451 | (dom-insert parent node vnext-sibling)
452 | this)
453 | (-velem-take [this]
454 | (dom-remove node))
455 | (-velem-replace [this old-velem]
456 | (if-let [old-node (ui/velem-native-element old-velem)]
457 | (do
458 | (r/dispose old-node)
459 | (if (text-node? old-node)
460 | (do
461 | (set! node old-node)
462 | (set! (.-textContent node) text))
463 | (do
464 | (.ensureNode this)
465 | (dom-simple-replace node old-node))))
466 | (do
467 | (.ensureNode this)
468 | (dom-remove-replace node old-velem)))
469 | this)
470 | (-velem-lifecycle-callback [this cb-name]))
471 |
472 | ;; Conversion of Clojure(script) DOM images to virtual DOM
473 |
474 | (defprotocol IDOMImage
475 | "A protocol for things that can be represented as virtual DOM or contain DOM nodes.
476 |
477 | Can be used to define custom conversions to DOM nodes or text for things such as numbers
478 | or dates; or can be used to define containers for DOM elements themselves."
479 | (-get-dom-image [x]
480 | "Should return either virtual DOM (a vector or string) or an actual DOM node."))
481 |
482 | (extend-protocol IDOMImage
483 | object
484 | (-get-dom-image [x] (str x))
485 |
486 | nil
487 | (-get-dom-image [x] "")
488 |
489 | boolean
490 | (-get-dom-image [x] (str x))
491 |
492 | number
493 | (-get-dom-image [x] (str x)))
494 |
495 | ;; From hiccup.compiler:
496 | (def ^{:doc "Regular expression that parses a CSS-style id and class from an element name."
497 | :private true}
498 | re-tag #"([^\s\.#]+)(?:#([^\s\.#]+))?(?:\.([^\s#]+))?")
499 |
500 | (def ^:private re-dot (js/RegExp. "\\." "g"))
501 |
502 | (declare as-velem)
503 |
504 | (defn- append-children* [velem-fn]
505 | (fn append-ch [ch-res children]
506 | (doseq [ch children]
507 | (if (sequential? ch)
508 | (let [head (first ch)]
509 | (cond
510 | (or (keyword? head) (and (ifn? head) (not (sequential? head))))
511 | (.push ch-res (velem-fn ch))
512 |
513 | :default
514 | (append-ch ch-res ch)))
515 | (.push ch-res (velem-fn ch))))
516 | ch-res))
517 |
518 | (defn element* [elem-factory append-children-fn]
519 | (fn [ns-uri tag tail]
520 | (let [[_ tag-name id class] (re-matches re-tag tag)
521 | attrs? (first tail)
522 | have-attrs (map? attrs?)
523 | attrs (if have-attrs attrs? {})
524 | attrs (cond-> attrs
525 |
526 | (and id (not (:id attrs)))
527 | (assoc :id id)
528 |
529 | class
530 | (update :class
531 | (fn [cls]
532 | (let [class (.replace class re-dot " ")]
533 | (if cls (str class " " cls) class)))))
534 |
535 | children (if have-attrs (rest tail) tail)
536 | children* (append-children-fn #js [] children)]
537 | (elem-factory ns-uri tag-name attrs children*))))
538 |
539 | (defn- dom-node? [x]
540 | (and x (> (.-nodeType x) 0)))
541 |
542 | (defn managed? [elem]
543 | (if (get-element-state elem) true false))
544 |
545 | (defn- ensure-unmanaged [elem]
546 | (assert (dom-node? elem))
547 | (assert (not (managed? elem)))
548 | elem)
549 |
550 | (defn wrap-unmanaged-node [dom-node on-dispose]
551 | (ensure-unmanaged dom-node)
552 | (let [state (UnmanagedDOMNode. dom-node on-dispose nil)]
553 | (set! (.-freactive-state dom-node) state)
554 | state))
555 |
556 | (declare dom-element)
557 |
558 | (defn- as-velem* [{:keys [node-ns-lookup attr-ns-lookup]}]
559 | (fn [elem-spec]
560 | (cond
561 | (string? elem-spec)
562 | (DOMTextNode. elem-spec nil nil)
563 |
564 | (dom-node? elem-spec)
565 | (if-let [state (get-element-state elem-spec)]
566 | state
567 | (UnmanagedDOMNode. elem-spec nil nil))
568 |
569 | (sequential? elem-spec)
570 | (let [head (first elem-spec)]
571 | (cond
572 | (keyword? head)
573 | (let [tag-ns (namespace head)
574 | tag-name (name head)
575 | tail (rest elem-spec)]
576 | (if tag-ns
577 | (if-let [tag-handler (aget node-ns-lookup tag-ns)]
578 | (cond
579 | (string? tag-handler)
580 | (dom-element tag-handler tag-name tail)
581 |
582 | (ifn? tag-handler)
583 | (as-velem (tag-handler tag-name tail))
584 |
585 | :default
586 | (.warn js/console "Invalid ns node handler" tag-handler))
587 | (.warn js/console "Undefined ns node prefix" tag-ns))
588 | (dom-element nil tag-name tail)))
589 |
590 | (and (ifn? head) (not (sequential? head)))
591 | (as-velem (r/rx* (fn [] (apply head (rest elem-spec)))))
592 |
593 | :default
594 | (as-velem (r/seq-projection elem-spec))))
595 |
596 | (satisfies? IDeref elem-spec)
597 | (ui/reactive-element elem-spec as-velem queue-animation)
598 |
599 | (satisfies? ui/IVirtualElement elem-spec)
600 | elem-spec
601 |
602 | (satisfies? r/IProjection elem-spec)
603 | (ui/reactive-element-sequence elem-spec as-velem queue-animation)
604 |
605 | (satisfies? r/IAsVirtualElement elem-spec)
606 | (as-velem (r/-as-velem elem-spec as-velem))
607 |
608 | :default (as-velem (-get-dom-image elem-spec)))))
609 |
610 | (defn- as-velem [elem-spec]
611 | (cond
612 | (string? elem-spec)
613 | (DOMTextNode. elem-spec nil nil)
614 |
615 | (dom-node? elem-spec)
616 | (if-let [state (get-element-state elem-spec)]
617 | state
618 | (UnmanagedDOMNode. elem-spec nil nil))
619 |
620 | (sequential? elem-spec)
621 | (let [head (first elem-spec)]
622 | (cond
623 | (keyword? head)
624 | (let [tag-ns (namespace head)
625 | tag-name (name head)
626 | tail (rest elem-spec)]
627 | (if tag-ns
628 | (if-let [tag-handler (aget node-ns-lookup tag-ns)]
629 | (cond
630 | (string? tag-handler)
631 | (dom-element tag-handler tag-name tail)
632 |
633 | (ifn? tag-handler)
634 | (as-velem (tag-handler tag-name tail))
635 |
636 | :default
637 | (.warn js/console "Invalid ns node handler" tag-handler))
638 | (.warn js/console "Undefined ns node prefix" tag-ns))
639 | (dom-element nil tag-name tail)))
640 |
641 | (and (ifn? head) (not (sequential? head)))
642 | (as-velem (r/rx* (fn [] (apply head (rest elem-spec)))))
643 |
644 | :default
645 | (as-velem (r/seq-projection elem-spec))))
646 |
647 | (satisfies? IDeref elem-spec)
648 | (ui/reactive-element elem-spec as-velem queue-animation)
649 |
650 | (satisfies? ui/IVirtualElement elem-spec)
651 | elem-spec
652 |
653 | (satisfies? r/IProjection elem-spec)
654 | (ui/reactive-element-sequence elem-spec as-velem queue-animation)
655 |
656 | (satisfies? r/IAsVirtualElement elem-spec)
657 | (as-velem (r/-as-velem elem-spec as-velem))
658 |
659 | :default (as-velem (-get-dom-image elem-spec))))
660 |
661 | (def dom-element
662 | (element*
663 | (fn [ns-uri tag-name attrs children*]
664 | (DOMElement. ns-uri tag-name attrs children* nil nil nil))
665 | (append-children* as-velem)))
666 |
667 |
668 | ;; Public API
669 |
670 | (defn- get-velem-state [elem]
671 | (or (get-element-state elem) elem))
672 |
673 | (defn- find-by-id [id]
674 | (.getElementById js/document id))
675 |
676 | (defn- get-managed-dom-element [elem]
677 | (let [velem (ui/velem-simple-element (get-element-state elem))]
678 | (assert (instance? DOMElement velem) "Not a managed DOM element.")
679 | velem))
680 |
681 | (defn set-attrs!
682 | "Sets the attributes on a managed element to the new-attrs map
683 | (unsetting previous values if not present in the new map)."
684 | [elem new-attrs]
685 | (let [velem (get-managed-dom-element elem)]
686 | (.updateAttrs velem new-attrs)))
687 |
688 | (defn merge-attrs! [elem attrs]
689 | "Merges the attrs map with a managed element's existing attribute map. A nil
690 | value will unset any attribute."
691 | (let [velem (get-managed-dom-element elem)]
692 | (.updateAttrs velem (merge-with merge (.-attrs velem) attrs))))
693 |
694 | (defn update-attrs! [elem f & args]
695 | "Updates a managed element's attributes by applying f and optionally args to
696 | the existing attribute map."
697 | (let [velem (get-managed-dom-element elem)]
698 | (.updateAttrs velem (apply f (.-attrs velem) args))))
699 |
700 | (defn- create-or-find-root-node [id]
701 | (if-let [root-node (find-by-id id)]
702 | root-node
703 | (let [root-node (.createElement js/document "div")]
704 | (set! (.-id root-node) id)
705 | (.appendChild (.-body js/document) root-node))))
706 |
707 | (defn- configure-root! [vroot vdom]
708 | (let [velem (as-velem vdom)]
709 | (set! (.-children vroot) #js [velem])
710 | (ui/velem-insert velem vroot nil)))
711 |
712 | (defn- do-unmount! [vroot]
713 | (assert (instance? DOMElement vroot) "Can only unmount from managed elements")
714 | (doseq [child (.-children vroot)]
715 | (ui/velem-remove child))
716 | (set! (.-children vroot) nil)
717 | ;; (let [root (.-root vroot)]
718 | ;; (when-not (keyword-identical? :unmounted root)
719 | ;; (ui/velem-remove root)
720 | ;; (set! (.-root vroot) :unmounted)))
721 | )
722 |
723 | (defn mount! [mount-point vdom]
724 | "Makes the specified mount-point the root of a managed element tree, replacing
725 | all of its content with the managed content specified by vdom.
726 | mount-point may be an unmanaged DOM element or a string specifying the id of one.
727 | If a string id is passed and no matching node is found, one will be appended to the
728 | document body."
729 | (let [root-node
730 | (cond
731 | (dom-node? mount-point)
732 | mount-point
733 |
734 | (string? mount-point)
735 | (create-or-find-root-node mount-point))]
736 | (if-let [vroot (get-element-state root-node)]
737 | (do
738 | ;; (assert (.-root vroot) "Can only remount at a previous mount point")
739 | (do-unmount! vroot)
740 | (configure-root! vroot vdom))
741 | (do
742 | (loop []
743 | (let [last-child (.-lastChild root-node)]
744 | (when last-child
745 | (ui/velem-remove (as-velem (get-velem-state last-child)))
746 | (recur))))
747 | (let [vroot (DOMElement.
748 | (.-namespaceURI root-node)
749 | (.-tagName root-node)
750 | {}
751 | nil ;; TODO parent
752 | root-node
753 | nil
754 | (bind-attrs! root-node {}))]
755 | (set! (.-freactive-state root-node) vroot)
756 | (configure-root! vroot vdom))))
757 | root-node))
758 |
759 | (defn unmount! [mount-point]
760 | (let [root-node
761 | (cond
762 | (dom-node? mount-point)
763 | mount-point
764 |
765 | (string? mount-point)
766 | (find-by-id mount-point))
767 | vroot (get-element-state root-node)]
768 | ;; (assert (and root-node vroot (.-root vroot)) "Can't find mount point")
769 | (do-unmount! vroot)
770 | root-node))
771 |
--------------------------------------------------------------------------------