├── .gitignore ├── LICENSE ├── README.md ├── example └── freactive │ ├── diff_perf.cljs │ ├── dom_perf.cljs │ ├── elem_seqs.cljs │ └── test1.cljs ├── project.clj ├── resources └── public │ ├── advanced.html │ ├── index.html │ └── js │ └── .gitignore └── src └── freactive ├── animation.cljs ├── dom.cljs ├── plugins ├── garden.cljs └── goog_events.cljs ├── react.cljs └── ui_common.cljs /.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 -------------------------------------------------------------------------------- /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 | [![Clojars Project](http://clojars.org/freactive/latest-version.svg)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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! % :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 | -------------------------------------------------------------------------------- /resources/public/advanced.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /resources/public/js/.gitignore: -------------------------------------------------------------------------------- 1 | compiled/ -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/freactive/ui_common.cljs: -------------------------------------------------------------------------------- 1 | (ns freactive.ui-common 2 | (:require 3 | [freactive.core :as r])) 4 | 5 | (defprotocol IVirtualElement 6 | "Warning: this is currently an internal API subject to change or sudden removal. 7 | 8 | A simple element is any element which directly wraps exactly one native element." 9 | (-velem-insert [this vparent vnext-sibling] 10 | "Inserts this virtual element into a native element tree. vnext-sibling 11 | is the next sibling virtual element if any.") 12 | (-velem-replace [this cur-velem] 13 | "Replaces cur-velem (making sure to dispose it) with this virtual element.") 14 | (-velem-take [this] 15 | "Removes the virtual element from the element tree without disposing it.") 16 | (-velem-parent [this] 17 | "Returns the virtual parent element") 18 | (-velem-head [this] 19 | "Returns a simple virtual element (or nil) representing the head of an 20 | element sequence. Same as -velem-simple-element for simple elements.") 21 | (-velem-next-sibling-of [this child] 22 | "Returns the next virtual sibling element to this element or nil when this 23 | is the last child.") 24 | (-velem-native-element [this] 25 | "Returns the native element wrapped by this simple element or nil for 26 | sequence elements.") 27 | (-velem-simple-element [this] 28 | "Returns the simplest virtual element (one directly wrapping a native 29 | element) wrapped by this virtual element (or itself), or nil for sequence 30 | elements.") 31 | (-velem-lifecycle-callback [this cb-name] 32 | "Gets a lifecycle callback fn (if any) for the keyword cb-name.")) 33 | 34 | (defn velem-native-element [this] 35 | (-velem-native-element this)) 36 | 37 | (defn velem-simple-element [this] 38 | (-velem-simple-element this)) 39 | 40 | (defn velem-parent [this] 41 | (-velem-parent this)) 42 | 43 | (defn velem-head [this] 44 | (-velem-head this)) 45 | 46 | (defn velem-next-sibling-of [this child] 47 | (-velem-next-sibling-of this child)) 48 | 49 | (defn next-native-sibling [vnext] 50 | (when vnext 51 | (if-let [head (velem-head vnext)] 52 | (velem-native-element head) 53 | (let [parent (velem-parent vnext)] 54 | (loop [vnext vnext] 55 | (let [vnnext (velem-next-sibling-of parent vnext)] 56 | (if vnnext 57 | (if-let [head (velem-head vnnext)] 58 | (velem-native-element head) 59 | (recur (velem-next-sibling-of parent vnnext))) 60 | (when-not (velem-simple-element parent) 61 | (let [pparent (velem-parent parent)] 62 | (next-native-sibling (velem-next-sibling-of pparent parent))))))))))) 63 | 64 | (defn native-parent [parent] 65 | (when parent 66 | (or (velem-native-element parent) 67 | (native-parent (velem-parent parent))))) 68 | 69 | (defn velem-take [this] 70 | (-velem-take this)) 71 | 72 | (defn velem-remove [this] 73 | (velem-take this) 74 | (r/dispose this)) 75 | 76 | (defn velem-insert [this vparent vnext-sibling] 77 | (-velem-insert this vparent vnext-sibling)) 78 | 79 | (defn velem-replace [this cur-velem] 80 | (-velem-replace this cur-velem)) 81 | 82 | (defn velem-lifecycle-callback [this cb-name] 83 | (-velem-lifecycle-callback this cb-name)) 84 | 85 | (deftype ReactiveElement [id the-ref binding-info velem-fn enqueue-fn 86 | ^:mutable parent ^:mutable vnext 87 | ^:mutable cur-velem ^:mutable dirty 88 | ^:mutable updating ^:mutable disposed] 89 | Object 90 | (onInitialized [this] 91 | (when-let [binding-initialized (get (meta the-ref) :binding/on-initialized)] 92 | (binding-initialized the-ref))) 93 | (dispose [this] 94 | ((.-remove-watch binding-info) the-ref id) 95 | (when cur-velem (r/dispose cur-velem)) 96 | (when-let [clean (.-clean binding-info)] (clean the-ref)) 97 | (set! (.-disposed this) true) 98 | (when-let [binding-disposed (get (meta the-ref) :binding/on-disposed)] 99 | (binding-disposed the-ref))) 100 | (get-new-elem [this] 101 | (set! dirty false) 102 | ((.-add-watch binding-info) the-ref id #(.invalidate this)) 103 | (velem-fn (or ((.-raw-deref binding-info) the-ref) ""))) 104 | (done-updating [this] 105 | (set! updating false) 106 | (when dirty 107 | (enqueue-fn #(.animate this)))) 108 | (show-new-elem [this new-velem] 109 | (set! cur-velem 110 | (if cur-velem 111 | (velem-replace new-velem cur-velem) 112 | (velem-insert new-velem parent vnext))) 113 | (.done-updating this)) 114 | (animate [this] 115 | (when-not disposed 116 | (let [new-velem (.get-new-elem this)] 117 | (when-not (= new-velem cur-velem) 118 | (if-let [on-detaching (velem-lifecycle-callback cur-velem :node/on-detaching)] 119 | (on-detaching 120 | cur-velem 121 | (fn [] 122 | (if disposed 123 | (set! updating false) 124 | (.show-new-elem this (if dirty 125 | (.get-new-elem this) 126 | new-velem))))) 127 | (.show-new-elem this new-velem)))))) 128 | (invalidate [this] 129 | ((.-remove-watch binding-info) the-ref id) 130 | (when-not disposed 131 | (set! dirty true) 132 | (when-not updating 133 | (set! updating true) 134 | (enqueue-fn #(.animate this))))) 135 | 136 | IEquiv 137 | (-equiv [this other] 138 | (and 139 | (instance? ReactiveElement other) 140 | (= the-ref (.-the-ref other)))) 141 | 142 | IVirtualElement 143 | (-velem-parent [this] parent) 144 | (-velem-head [this] (velem-head cur-velem)) 145 | (-velem-next-sibling-of [this child] 146 | (velem-next-sibling-of cur-velem child)) 147 | (-velem-native-element [this] (velem-native-element cur-velem)) 148 | (-velem-simple-element [this] (velem-simple-element cur-velem)) 149 | (-velem-insert [this vparent vnext-sibling] 150 | (set! (.-parent this) vparent) 151 | (set! (.-vnext this) vnext-sibling) 152 | (.onInitialized this) 153 | (.show-new-elem this (.get-new-elem this)) 154 | this) 155 | (-velem-take [this] 156 | (velem-take cur-velem)) 157 | (-velem-replace [this elem-to-replace] 158 | (.onInitialized this) 159 | (set! cur-velem (velem-replace (.get-new-elem this) elem-to-replace)) 160 | this) 161 | (-velem-lifecycle-callback [this cb-name] 162 | (when cur-velem 163 | (velem-lifecycle-callback cur-velem cb-name)))) 164 | 165 | (defn reactive-element [the-ref velem-fn enqueue-fn] 166 | (ReactiveElement. 167 | (r/new-reactive-id) 168 | the-ref 169 | (r/get-binding-fns the-ref) 170 | velem-fn 171 | enqueue-fn 172 | nil nil nil false false false)) 173 | 174 | ;; (defprotocol IVirtualElementContainer 175 | ;; (-velem-container-insert [this child before]) 176 | ;; (-velem-container-remove [this child])) 177 | 178 | (defn array-next-sibling-of [elements child] 179 | (let [idx (.indexOf elements child)] 180 | (when (and (>= 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 | --------------------------------------------------------------------------------